feat(core): use custom resolution to resolve from source local plugins 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 #
This commit is contained in:
parent
5bdda1daac
commit
48cd50a550
@ -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
|
||||
|
||||
@ -28,4 +28,4 @@ Additional metadata about a project
|
||||
|
||||
### type
|
||||
|
||||
• **type**: `"app"` \| `"e2e"` \| `"lib"`
|
||||
• **type**: `"lib"` \| `"app"` \| `"e2e"`
|
||||
|
||||
193
e2e/plugin/src/nx-plugin-ts-solution.test.ts
Normal file
193
e2e/plugin/src/nx-plugin-ts-solution.test.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -50,7 +50,7 @@ module.exports = function (path: string, options: ResolverOptions) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return resolveExports(pkg, path) || path;
|
||||
return resolveExports(pkg, path)?.[0] || path;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1183,7 +1183,9 @@ async function getWrappedWorkspaceNodeModulesArchitectHost(
|
||||
optionSchema: builderInfo.schema,
|
||||
import: resolveImplementation(
|
||||
executorConfig.implementation,
|
||||
dirname(executorsFilePath)
|
||||
dirname(executorsFilePath),
|
||||
packageName,
|
||||
this.projects
|
||||
),
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 = {};
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { existsSync } from 'fs';
|
||||
import { extname, join } from 'path';
|
||||
import { resolve as resolveExports } from 'resolve.exports';
|
||||
import { getPackageEntryPointsToProjectMap } from '../plugins/js/utils/packages';
|
||||
import { registerPluginTSTranspiler } from '../project-graph/plugins';
|
||||
import { normalizePath } from '../utils/path';
|
||||
import type { ProjectConfiguration } from './workspace-json-project-json';
|
||||
|
||||
/**
|
||||
* This function is used to get the implementation factory of an executor or generator.
|
||||
@ -10,14 +14,18 @@ import { registerPluginTSTranspiler } from '../project-graph/plugins';
|
||||
*/
|
||||
export function getImplementationFactory<T>(
|
||||
implementation: string,
|
||||
directory: string
|
||||
directory: string,
|
||||
packageName: string,
|
||||
projects: Record<string, ProjectConfiguration>
|
||||
): () => T {
|
||||
const [implementationModulePath, implementationExportName] =
|
||||
implementation.split('#');
|
||||
return () => {
|
||||
const modulePath = resolveImplementation(
|
||||
implementationModulePath,
|
||||
directory
|
||||
directory,
|
||||
packageName,
|
||||
projects
|
||||
);
|
||||
if (extname(modulePath) === '.ts') {
|
||||
registerPluginTSTranspiler();
|
||||
@ -37,12 +45,31 @@ export function getImplementationFactory<T>(
|
||||
*/
|
||||
export function resolveImplementation(
|
||||
implementationModulePath: string,
|
||||
directory: string
|
||||
directory: string,
|
||||
packageName: string,
|
||||
projects: Record<string, ProjectConfiguration>
|
||||
): string {
|
||||
const validImplementations = ['', '.js', '.ts'].map(
|
||||
(x) => implementationModulePath + x
|
||||
);
|
||||
|
||||
if (!directory.includes('node_modules')) {
|
||||
// It might be a local plugin where the implementation path points to the
|
||||
// outputs which might not exist or can be stale. We prioritize finding
|
||||
// the implementation from the source over the outputs.
|
||||
for (const maybeImplementation of validImplementations) {
|
||||
const maybeImplementationFromSource = tryResolveFromSource(
|
||||
maybeImplementation,
|
||||
directory,
|
||||
packageName,
|
||||
projects
|
||||
);
|
||||
if (maybeImplementationFromSource) {
|
||||
return maybeImplementationFromSource;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const maybeImplementation of validImplementations) {
|
||||
const maybeImplementationPath = join(directory, maybeImplementation);
|
||||
if (existsSync(maybeImplementationPath)) {
|
||||
@ -61,7 +88,27 @@ export function resolveImplementation(
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveSchema(schemaPath: string, directory: string): string {
|
||||
export function resolveSchema(
|
||||
schemaPath: string,
|
||||
directory: string,
|
||||
packageName: string,
|
||||
projects: Record<string, ProjectConfiguration>
|
||||
): string {
|
||||
if (!directory.includes('node_modules')) {
|
||||
// It might be a local plugin where the schema path points to the outputs
|
||||
// which might not exist or can be stale. We prioritize finding the schema
|
||||
// from the source over the outputs.
|
||||
const schemaPathFromSource = tryResolveFromSource(
|
||||
schemaPath,
|
||||
directory,
|
||||
packageName,
|
||||
projects
|
||||
);
|
||||
if (schemaPathFromSource) {
|
||||
return schemaPathFromSource;
|
||||
}
|
||||
}
|
||||
|
||||
const maybeSchemaPath = join(directory, schemaPath);
|
||||
if (existsSync(maybeSchemaPath)) {
|
||||
return maybeSchemaPath;
|
||||
@ -71,3 +118,60 @@ export function resolveSchema(schemaPath: string, directory: string): string {
|
||||
paths: [directory],
|
||||
});
|
||||
}
|
||||
|
||||
let packageEntryPointsToProjectMap: Record<string, ProjectConfiguration>;
|
||||
function tryResolveFromSource(
|
||||
path: string,
|
||||
directory: string,
|
||||
packageName: string,
|
||||
projects: Record<string, ProjectConfiguration>
|
||||
): string | null {
|
||||
packageEntryPointsToProjectMap ??=
|
||||
getPackageEntryPointsToProjectMap(projects);
|
||||
const localProject = packageEntryPointsToProjectMap[packageName];
|
||||
if (!localProject) {
|
||||
// it doesn't match any of the package names from the local projects
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const fromExports = resolveExports(
|
||||
{
|
||||
name: localProject.metadata!.js!.packageName,
|
||||
exports: localProject.metadata!.js!.packageExports,
|
||||
},
|
||||
path,
|
||||
{ conditions: ['development'] }
|
||||
);
|
||||
if (fromExports && fromExports.length) {
|
||||
for (const exportPath of fromExports) {
|
||||
if (existsSync(join(directory, exportPath))) {
|
||||
return join(directory, exportPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
/**
|
||||
* Fall back to try to "guess" the source by checking the path in some common directories:
|
||||
* - the root of the project
|
||||
* - the src directory
|
||||
* - the src/lib directory
|
||||
*/
|
||||
const segments = normalizePath(path).replace(/^\.\//, '').split('/');
|
||||
for (let i = 1; i < segments.length; i++) {
|
||||
const possiblePaths = [
|
||||
join(directory, ...segments.slice(i)),
|
||||
join(directory, 'src', ...segments.slice(i)),
|
||||
join(directory, 'src', 'lib', ...segments.slice(i)),
|
||||
];
|
||||
|
||||
for (const possiblePath of possiblePaths) {
|
||||
if (existsSync(possiblePath)) {
|
||||
return possiblePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -57,6 +57,9 @@ describe('Workspaces', () => {
|
||||
{
|
||||
"metadata": {
|
||||
"description": "my-package description",
|
||||
"js": {
|
||||
"packageName": "my-package",
|
||||
},
|
||||
"targetGroups": {},
|
||||
},
|
||||
"name": "my-package",
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { PackageJson } from '../utils/package-json';
|
||||
import type {
|
||||
NxJsonConfiguration,
|
||||
NxReleaseVersionConfiguration,
|
||||
@ -136,6 +137,10 @@ export interface ProjectMetadata {
|
||||
}[];
|
||||
};
|
||||
};
|
||||
js?: {
|
||||
packageName: string;
|
||||
packageExports: undefined | PackageJson['exports'];
|
||||
};
|
||||
}
|
||||
|
||||
export interface TargetMetadata {
|
||||
|
||||
26
packages/nx/src/plugins/js/utils/packages.ts
Normal file
26
packages/nx/src/plugins/js/utils/packages.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { join } from 'node:path/posix';
|
||||
import type { ProjectConfiguration } from '../../../config/workspace-json-project-json';
|
||||
|
||||
export function getPackageEntryPointsToProjectMap(
|
||||
projects: Record<string, ProjectConfiguration>
|
||||
): Record<string, ProjectConfiguration> {
|
||||
const result: Record<string, ProjectConfiguration> = {};
|
||||
for (const project of Object.values(projects)) {
|
||||
if (!project.metadata?.js) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { packageName, packageExports } = project.metadata.js;
|
||||
if (!packageExports || typeof packageExports === 'string') {
|
||||
// no `exports` or it points to a file, which would be the equivalent of
|
||||
// an '.' export, in which case the package name is the entry point
|
||||
result[packageName] = project;
|
||||
} else {
|
||||
for (const entryPoint of Object.keys(packageExports)) {
|
||||
result[join(packageName, entryPoint)] = project;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -55,6 +55,10 @@ describe('nx package.json workspaces plugin', () => {
|
||||
".": {
|
||||
"metadata": {
|
||||
"description": undefined,
|
||||
"js": {
|
||||
"packageExports": undefined,
|
||||
"packageName": "root",
|
||||
},
|
||||
"targetGroups": {
|
||||
"NPM Scripts": [
|
||||
"echo",
|
||||
@ -98,6 +102,10 @@ describe('nx package.json workspaces plugin', () => {
|
||||
"packages/lib-a": {
|
||||
"metadata": {
|
||||
"description": "lib-a description",
|
||||
"js": {
|
||||
"packageExports": undefined,
|
||||
"packageName": "lib-a",
|
||||
},
|
||||
"targetGroups": {
|
||||
"NPM Scripts": [
|
||||
"test",
|
||||
@ -148,6 +156,10 @@ describe('nx package.json workspaces plugin', () => {
|
||||
],
|
||||
"metadata": {
|
||||
"description": "lib-b description",
|
||||
"js": {
|
||||
"packageExports": undefined,
|
||||
"packageName": "lib-b",
|
||||
},
|
||||
"targetGroups": {
|
||||
"NPM Scripts": [
|
||||
"build",
|
||||
@ -252,6 +264,10 @@ describe('nx package.json workspaces plugin', () => {
|
||||
"packages/vite": {
|
||||
"metadata": {
|
||||
"description": undefined,
|
||||
"js": {
|
||||
"packageExports": undefined,
|
||||
"packageName": "vite",
|
||||
},
|
||||
"targetGroups": {},
|
||||
},
|
||||
"name": "vite",
|
||||
@ -350,6 +366,10 @@ describe('nx package.json workspaces plugin', () => {
|
||||
"packages/vite": {
|
||||
"metadata": {
|
||||
"description": undefined,
|
||||
"js": {
|
||||
"packageExports": undefined,
|
||||
"packageName": "vite",
|
||||
},
|
||||
"targetGroups": {},
|
||||
},
|
||||
"name": "vite",
|
||||
@ -444,6 +464,10 @@ describe('nx package.json workspaces plugin', () => {
|
||||
"packages/vite": {
|
||||
"metadata": {
|
||||
"description": undefined,
|
||||
"js": {
|
||||
"packageExports": undefined,
|
||||
"packageName": "vite",
|
||||
},
|
||||
"targetGroups": {},
|
||||
},
|
||||
"name": "vite",
|
||||
@ -522,6 +546,10 @@ describe('nx package.json workspaces plugin', () => {
|
||||
"packages/a": {
|
||||
"metadata": {
|
||||
"description": undefined,
|
||||
"js": {
|
||||
"packageExports": undefined,
|
||||
"packageName": "root",
|
||||
},
|
||||
"targetGroups": {
|
||||
"NPM Scripts": [
|
||||
"build",
|
||||
@ -600,6 +628,10 @@ describe('nx package.json workspaces plugin', () => {
|
||||
"packages/a": {
|
||||
"metadata": {
|
||||
"description": undefined,
|
||||
"js": {
|
||||
"packageExports": undefined,
|
||||
"packageName": "root",
|
||||
},
|
||||
"targetGroups": {
|
||||
"NPM Scripts": [
|
||||
"build",
|
||||
@ -685,6 +717,10 @@ describe('nx package.json workspaces plugin', () => {
|
||||
"packages/a": {
|
||||
"metadata": {
|
||||
"description": undefined,
|
||||
"js": {
|
||||
"packageExports": undefined,
|
||||
"packageName": "root",
|
||||
},
|
||||
"targetGroups": {},
|
||||
},
|
||||
"name": "root",
|
||||
@ -796,4 +832,72 @@ describe('nx package.json workspaces plugin', () => {
|
||||
].projectType
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should store package name and exports in the project metadata', () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'packages/lib-a/package.json': JSON.stringify({
|
||||
name: 'lib-a',
|
||||
description: 'lib-a description',
|
||||
scripts: { test: 'jest' },
|
||||
exports: {
|
||||
'./package.json': './package.json',
|
||||
'.': './dist/index.js',
|
||||
},
|
||||
}),
|
||||
},
|
||||
'/root'
|
||||
);
|
||||
|
||||
expect(
|
||||
createNodeFromPackageJson('packages/lib-a/package.json', '/root', {})
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {
|
||||
"packages/lib-a": {
|
||||
"metadata": {
|
||||
"description": "lib-a description",
|
||||
"js": {
|
||||
"packageExports": {
|
||||
".": "./dist/index.js",
|
||||
"./package.json": "./package.json",
|
||||
},
|
||||
"packageName": "lib-a",
|
||||
},
|
||||
"targetGroups": {
|
||||
"NPM Scripts": [
|
||||
"test",
|
||||
],
|
||||
},
|
||||
},
|
||||
"name": "lib-a",
|
||||
"root": "packages/lib-a",
|
||||
"sourceRoot": "packages/lib-a",
|
||||
"tags": [
|
||||
"npm:public",
|
||||
],
|
||||
"targets": {
|
||||
"nx-release-publish": {
|
||||
"dependsOn": [
|
||||
"^nx-release-publish",
|
||||
],
|
||||
"executor": "@nx/js:release-publish",
|
||||
"options": {},
|
||||
},
|
||||
"test": {
|
||||
"executor": "nx:run-script",
|
||||
"metadata": {
|
||||
"runCommand": "npm run test",
|
||||
"scriptContent": "jest",
|
||||
},
|
||||
"options": {
|
||||
"script": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@ -138,6 +138,12 @@ export function createNodeFromPackageJson(
|
||||
const hash = hashObject({
|
||||
...json,
|
||||
root: projectRoot,
|
||||
/**
|
||||
* Increment this number to force processing the package.json again. Do it
|
||||
* when the implementation of this plugin is changed and results in different
|
||||
* results for the same package.json contents.
|
||||
*/
|
||||
bust: 1,
|
||||
});
|
||||
|
||||
const cached = cache[hash];
|
||||
|
||||
@ -31,6 +31,7 @@ import { LoadPluginError } from '../error-types';
|
||||
import path = require('node:path/posix');
|
||||
import { readTsConfig } from '../../plugins/js/utils/typescript';
|
||||
import { loadResolvedNxPluginAsync } from './load-resolved-plugin';
|
||||
import { getPackageEntryPointsToProjectMap } from '../../plugins/js/utils/packages';
|
||||
|
||||
export function readPluginPackageJson(
|
||||
pluginName: string,
|
||||
@ -124,38 +125,48 @@ function lookupLocalPlugin(
|
||||
return { path: path.join(root, projectConfig.root), projectConfig };
|
||||
}
|
||||
|
||||
let packageEntryPointsToProjectMap: Record<string, ProjectConfiguration>;
|
||||
function findNxProjectForImportPath(
|
||||
importPath: string,
|
||||
projects: Record<string, ProjectConfiguration>,
|
||||
root = workspaceRoot
|
||||
): ProjectConfiguration | null {
|
||||
const tsConfigPaths: Record<string, string[]> = readTsConfigPaths(root);
|
||||
const possiblePaths = tsConfigPaths[importPath]?.map((p) =>
|
||||
normalizePath(path.relative(root, path.join(root, p)))
|
||||
);
|
||||
if (possiblePaths?.length) {
|
||||
const projectRootMappings: ProjectRootMappings = new Map();
|
||||
const possibleTsPaths =
|
||||
tsConfigPaths[importPath]?.map((p) =>
|
||||
normalizePath(path.relative(root, path.join(root, p)))
|
||||
) ?? [];
|
||||
|
||||
const projectRootMappings: ProjectRootMappings = new Map();
|
||||
if (possibleTsPaths.length) {
|
||||
const projectNameMap = new Map<string, ProjectConfiguration>();
|
||||
for (const projectRoot in projects) {
|
||||
const project = projects[projectRoot];
|
||||
projectRootMappings.set(project.root, project.name);
|
||||
projectNameMap.set(project.name, project);
|
||||
}
|
||||
for (const tsConfigPath of possiblePaths) {
|
||||
for (const tsConfigPath of possibleTsPaths) {
|
||||
const nxProject = findProjectForPath(tsConfigPath, projectRootMappings);
|
||||
if (nxProject) {
|
||||
return projectNameMap.get(nxProject);
|
||||
}
|
||||
}
|
||||
logger.verbose(
|
||||
'Unable to find local plugin',
|
||||
possiblePaths,
|
||||
projectRootMappings
|
||||
);
|
||||
throw new Error(
|
||||
'Unable to resolve local plugin with import path ' + importPath
|
||||
);
|
||||
}
|
||||
|
||||
packageEntryPointsToProjectMap ??=
|
||||
getPackageEntryPointsToProjectMap(projects);
|
||||
if (packageEntryPointsToProjectMap[importPath]) {
|
||||
return packageEntryPointsToProjectMap[importPath];
|
||||
}
|
||||
|
||||
logger.verbose(
|
||||
'Unable to find local plugin',
|
||||
possibleTsPaths,
|
||||
projectRootMappings
|
||||
);
|
||||
throw new Error(
|
||||
'Unable to resolve local plugin with import path ' + importPath
|
||||
);
|
||||
}
|
||||
|
||||
let tsconfigPaths: Record<string, string[]>;
|
||||
|
||||
@ -49,7 +49,13 @@ export interface PackageJson {
|
||||
| string
|
||||
| Record<
|
||||
string,
|
||||
string | { types?: string; require?: string; import?: string }
|
||||
| string
|
||||
| {
|
||||
types?: string;
|
||||
require?: string;
|
||||
import?: string;
|
||||
development?: string;
|
||||
}
|
||||
>;
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
@ -149,13 +155,17 @@ let packageManagerCommand: PackageManagerCommands | undefined;
|
||||
export function getMetadataFromPackageJson(
|
||||
packageJson: PackageJson
|
||||
): ProjectMetadata {
|
||||
const { scripts, nx, description } = packageJson ?? {};
|
||||
const { scripts, nx, description, name, exports } = packageJson;
|
||||
const includedScripts = nx?.includedScripts || Object.keys(scripts ?? {});
|
||||
return {
|
||||
targetGroups: {
|
||||
...(includedScripts.length ? { 'NPM Scripts': includedScripts } : {}),
|
||||
},
|
||||
description,
|
||||
js: {
|
||||
packageName: name,
|
||||
packageExports: exports,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@ -902,8 +902,8 @@ importers:
|
||||
specifier: ^8.5.3
|
||||
version: 8.5.3(@types/react@18.3.1)(react@18.3.1)
|
||||
resolve.exports:
|
||||
specifier: 1.1.0
|
||||
version: 1.1.0
|
||||
specifier: 2.0.3
|
||||
version: 2.0.3
|
||||
rollup:
|
||||
specifier: ^4.14.0
|
||||
version: 4.22.0
|
||||
@ -15058,8 +15058,8 @@ packages:
|
||||
resolution: {integrity: sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
resolve.exports@2.0.2:
|
||||
resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==}
|
||||
resolve.exports@2.0.3:
|
||||
resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
resolve@1.22.8:
|
||||
@ -28326,7 +28326,7 @@ snapshots:
|
||||
'@jspm/core': 2.0.1
|
||||
esbuild: 0.17.6
|
||||
local-pkg: 0.5.0
|
||||
resolve.exports: 2.0.2
|
||||
resolve.exports: 2.0.3
|
||||
|
||||
esbuild-register@3.6.0(esbuild@0.19.5):
|
||||
dependencies:
|
||||
@ -30895,7 +30895,7 @@ snapshots:
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
resolve: 1.22.8
|
||||
resolve.exports: 2.0.2
|
||||
resolve.exports: 2.0.3
|
||||
slash: 3.0.0
|
||||
|
||||
jest-runner@29.7.0:
|
||||
@ -35074,7 +35074,7 @@ snapshots:
|
||||
|
||||
resolve.exports@1.1.0: {}
|
||||
|
||||
resolve.exports@2.0.2: {}
|
||||
resolve.exports@2.0.3: {}
|
||||
|
||||
resolve@1.22.8:
|
||||
dependencies:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user