diff --git a/docs/generated/devkit/ProjectGraphProjectNode.md b/docs/generated/devkit/ProjectGraphProjectNode.md index 12a785f2aa..a641649d80 100644 --- a/docs/generated/devkit/ProjectGraphProjectNode.md +++ b/docs/generated/devkit/ProjectGraphProjectNode.md @@ -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"` diff --git a/e2e/plugin/src/nx-plugin-ts-solution.test.ts b/e2e/plugin/src/nx-plugin-ts-solution.test.ts new file mode 100644 index 0000000000..df3473383c --- /dev/null +++ b/e2e/plugin/src/nx-plugin-ts-solution.test.ts @@ -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(); + }); +}); diff --git a/package.json b/package.json index 2072f5688f..e8a85489ee 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/jest/package.json b/packages/jest/package.json index 438ec0c8e4..b378d8b72e 100644 --- a/packages/jest/package.json +++ b/packages/jest/package.json @@ -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" diff --git a/packages/jest/plugins/resolver.ts b/packages/jest/plugins/resolver.ts index 33c8b0303d..c6d316e68a 100644 --- a/packages/jest/plugins/resolver.ts +++ b/packages/jest/plugins/resolver.ts @@ -50,7 +50,7 @@ module.exports = function (path: string, options: ResolverOptions) { return path; } - return resolveExports(pkg, path) || path; + return resolveExports(pkg, path)?.[0] || path; }, }); } diff --git a/packages/nx/package.json b/packages/nx/package.json index 98ad418778..0703927b6c 100644 --- a/packages/nx/package.json +++ b/packages/nx/package.json @@ -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", diff --git a/packages/nx/src/adapter/ngcli-adapter.ts b/packages/nx/src/adapter/ngcli-adapter.ts index 3ceaf2e0f3..4636e3de10 100644 --- a/packages/nx/src/adapter/ngcli-adapter.ts +++ b/packages/nx/src/adapter/ngcli-adapter.ts @@ -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( executorConfig.implementation, - executorsDir + executorsDir, + nodeModule ); const batchImplementationFactory = executorConfig.batchImplementation ? this.getImplementationFactory( executorConfig.batchImplementation, - executorsDir + executorsDir, + nodeModule ) : null; const hasherFactory = executorConfig.hasher ? this.getImplementationFactory( executorConfig.hasher, - executorsDir + executorsDir, + nodeModule ) : null; @@ -1278,9 +1288,15 @@ async function getWrappedWorkspaceNodeModulesArchitectHost( private getImplementationFactory( implementation: string, - executorsDir: string + executorsDir: string, + packageName: string ): () => T { - return getImplementationFactory(implementation, executorsDir); + return getImplementationFactory( + implementation, + executorsDir, + packageName, + this.projects + ); } } diff --git a/packages/nx/src/command-line/generate/generator-utils.ts b/packages/nx/src/command-line/generate/generator-utils.ts index b5de6cef84..0e5feea33d 100644 --- a/packages/nx/src/command-line/generate/generator-utils.ts +++ b/packages/nx/src/command-line/generate/generator-utils.ts @@ -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( generatorConfig.implementation, - generatorsDir + generatorsDir, + collectionName, + projects ); const normalizedGeneratorConfiguration: GeneratorsJsonEntry = { ...generatorConfig, diff --git a/packages/nx/src/command-line/run/executor-utils.ts b/packages/nx/src/command-line/run/executor-utils.ts index 2840c1b536..d473e73261 100644 --- a/packages/nx/src/command-line/run/executor-utils.ts +++ b/packages/nx/src/command-line/run/executor-utils.ts @@ -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( executorConfig.implementation, - executorsDir + executorsDir, + nodeModule, + projects ); const batchImplementationFactory = executorConfig.batchImplementation ? getImplementationFactory( executorConfig.batchImplementation, - executorsDir + executorsDir, + nodeModule, + projects ) : null; const hasherFactory = executorConfig.hasher ? getImplementationFactory( executorConfig.hasher, - executorsDir + executorsDir, + nodeModule, + projects ) : null; diff --git a/packages/nx/src/config/schema-utils.ts b/packages/nx/src/config/schema-utils.ts index 30d7d08313..82692713e6 100644 --- a/packages/nx/src/config/schema-utils.ts +++ b/packages/nx/src/config/schema-utils.ts @@ -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( implementation: string, - directory: string + directory: string, + packageName: string, + projects: Record ): () => 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( */ export function resolveImplementation( implementationModulePath: string, - directory: string + directory: string, + packageName: string, + projects: Record ): 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 { + 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; +function tryResolveFromSource( + path: string, + directory: string, + packageName: string, + projects: Record +): 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; +} diff --git a/packages/nx/src/config/to-project-name.spec.ts b/packages/nx/src/config/to-project-name.spec.ts index 0815e9237b..b297b92a37 100644 --- a/packages/nx/src/config/to-project-name.spec.ts +++ b/packages/nx/src/config/to-project-name.spec.ts @@ -57,6 +57,9 @@ describe('Workspaces', () => { { "metadata": { "description": "my-package description", + "js": { + "packageName": "my-package", + }, "targetGroups": {}, }, "name": "my-package", diff --git a/packages/nx/src/config/workspace-json-project-json.ts b/packages/nx/src/config/workspace-json-project-json.ts index 2ae7a8ddcd..61132aa6fe 100644 --- a/packages/nx/src/config/workspace-json-project-json.ts +++ b/packages/nx/src/config/workspace-json-project-json.ts @@ -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 { diff --git a/packages/nx/src/plugins/js/utils/packages.ts b/packages/nx/src/plugins/js/utils/packages.ts new file mode 100644 index 0000000000..a39c794fe6 --- /dev/null +++ b/packages/nx/src/plugins/js/utils/packages.ts @@ -0,0 +1,26 @@ +import { join } from 'node:path/posix'; +import type { ProjectConfiguration } from '../../../config/workspace-json-project-json'; + +export function getPackageEntryPointsToProjectMap( + projects: Record +): Record { + const result: Record = {}; + 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; +} diff --git a/packages/nx/src/plugins/package-json/create-nodes.spec.ts b/packages/nx/src/plugins/package-json/create-nodes.spec.ts index a83cf0a183..eafa255ec1 100644 --- a/packages/nx/src/plugins/package-json/create-nodes.spec.ts +++ b/packages/nx/src/plugins/package-json/create-nodes.spec.ts @@ -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", + }, + }, + }, + }, + }, + } + `); + }); }); diff --git a/packages/nx/src/plugins/package-json/create-nodes.ts b/packages/nx/src/plugins/package-json/create-nodes.ts index b54d87681a..e30202d053 100644 --- a/packages/nx/src/plugins/package-json/create-nodes.ts +++ b/packages/nx/src/plugins/package-json/create-nodes.ts @@ -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]; diff --git a/packages/nx/src/project-graph/plugins/loader.ts b/packages/nx/src/project-graph/plugins/loader.ts index 0e90af7685..8ca7153d81 100644 --- a/packages/nx/src/project-graph/plugins/loader.ts +++ b/packages/nx/src/project-graph/plugins/loader.ts @@ -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; function findNxProjectForImportPath( importPath: string, projects: Record, root = workspaceRoot ): ProjectConfiguration | 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: 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(); 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; diff --git a/packages/nx/src/utils/package-json.ts b/packages/nx/src/utils/package-json.ts index ea6dda74d0..8efdd742e3 100644 --- a/packages/nx/src/utils/package-json.ts +++ b/packages/nx/src/utils/package-json.ts @@ -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; devDependencies?: Record; @@ -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, + }, }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4502f6e906..0b45932cfa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: