Skip to main content
Glama
nrwl

Nx MCP Server

Official
by nrwl
read-collections.ts12.2 kB
import { readJsonFile, directoryExists } from '@nx-console/shared-file-system'; import { packageDetails, workspaceDependencies, workspaceDependencyPath, } from '@nx-console/shared-npm'; import { CollectionInfo, ExecutorCollectionInfo, Generator, GeneratorCollectionInfo, GeneratorType, } from '@nx-console/shared-schema'; import { platform } from 'os'; import { dirname, resolve, join } from 'path'; import { nxWorkspace } from './workspace'; import { Logger } from '@nx-console/shared-utils'; export type ReadCollectionsOptions = { includeHidden?: boolean; includeNgAdd?: boolean; }; export async function readCollections( workspacePath: string, options: ReadCollectionsOptions, logger?: Logger, ): Promise<CollectionInfo[]> { const { projectGraph, nxVersion } = await nxWorkspace(workspacePath, logger); const packages = await workspaceDependencies( workspacePath, nxVersion, projectGraph.nodes, ); const collections = await Promise.all( packages.map(async (p) => { return await packageDetails(p); }), ); // Expand collections to include export-based secondary entry points const expandedCollections = await Promise.all( collections.map(async (c) => { let secondaryEntryPoints: { packagePath: string; packageName: string; packageJson: any; }[] = []; secondaryEntryPoints = await getExportBasedSecondaryEntryPoints(c); return [c, ...secondaryEntryPoints]; }), ); const allCollections = ( await Promise.all( expandedCollections .flat() .map((c) => readCollection(workspacePath, c, options)), ) ).flat(); /** * Since we gather all collections, and collections listed in `extends`, we need to dedupe collections here if workspaces have that extended collection in their own package.json */ const dedupedCollections = new Map<string, CollectionInfo>(); for (const singleCollection of allCollections) { if (!singleCollection) { continue; } if ( !dedupedCollections.has( collectionNameWithType(singleCollection.name, singleCollection.type), ) ) { dedupedCollections.set( collectionNameWithType(singleCollection.name, singleCollection.type), singleCollection, ); } } return Array.from(dedupedCollections.values()); } async function readCollection( workspacePath: string, { packagePath, packageName, packageJson: json, }: { packagePath: string; packageName: string; packageJson: any; }, options: ReadCollectionsOptions, ): Promise<CollectionInfo[] | null> { try { const [executorCollections, generatorCollections] = await Promise.all([ readJsonFile(json.executors || json.builders, packagePath), readJsonFile(json.generators || json.schematics, packagePath), ]); return getCollectionInfo( workspacePath, packageName, packagePath, executorCollections, generatorCollections, options, json, // Pass the package.json for secondary entry point filtering ); } catch (e) { return null; } } export async function getCollectionInfo( workspacePath: string, collectionName: string, collectionPath: string, executorCollection: { path: string; json: any }, generatorCollection: { path: string; json: any }, options: ReadCollectionsOptions, packageJson?: any, // Package.json for secondary entry point filtering ): Promise<CollectionInfo[]> { const collectionMap: Map<string, CollectionInfo> = new Map(); const executors = { ...executorCollection.json.executors, ...executorCollection.json.builders, }; for (const key of Object.keys(executors)) { let schema = executors[key]; if (typeof schema === 'string') { schema = await resolveDelegatedExecutor(schema, workspacePath); if (!schema) { continue; } } if (!canUse(key, schema, options.includeHidden, options.includeNgAdd)) { continue; } const collectionInfo: ExecutorCollectionInfo = { type: 'executor', name: `${collectionName}:${key}`, schemaPath: formatCollectionPath( collectionPath, executorCollection.path, schema.schema, ), implementationPath: formatCollectionPath( collectionPath, executorCollection.path, schema.implementation, ), configPath: formatPath(resolve(collectionPath, executorCollection.path)), collectionName, }; if ( collectionMap.has(collectionNameWithType(collectionInfo.name, 'executor')) ) { continue; } collectionMap.set( collectionNameWithType(collectionInfo.name, 'executor'), collectionInfo, ); } const generators = { ...generatorCollection.json.generators, ...generatorCollection.json.schematics, }; for (const [key, schema] of Object.entries<any>(generators)) { if (!canUse(key, schema, options.includeHidden, options.includeNgAdd)) { continue; } // Filter generators for secondary entry points if (packageJson?.__secondaryEntryPoint) { const secondaryEntryPoint = packageJson.__secondaryEntryPoint; const schemaPath = schema.schema; // Only include generators that are under the secondary entry point's path if (!schemaPath.includes(`/src/${secondaryEntryPoint}/`)) { continue; } } try { const collectionInfo: GeneratorCollectionInfo = { type: 'generator', name: `${collectionName}:${key}`, schemaPath: formatCollectionPath( collectionPath, generatorCollection.path, schema.schema, ), configPath: formatPath( resolve(collectionPath, generatorCollection.path), ), data: readCollectionGenerator(collectionName, key, schema), collectionName, }; if ( collectionMap.has( collectionNameWithType(collectionInfo.name, 'generator'), ) ) { continue; } collectionMap.set( collectionNameWithType(collectionInfo.name, 'generator'), collectionInfo, ); } catch (e) { // noop - generator is invalid } } if ( generatorCollection.json.extends && Array.isArray(generatorCollection.json.extends) ) { const extendedSchema = generatorCollection.json.extends as string[]; const extendedCollections = ( await Promise.all( extendedSchema .filter( (extended) => extended !== '@nx/workspace' && extended !== '@nrwl/workspace', ) .map(async (extended: string) => { const dependencyPath = await workspaceDependencyPath( workspacePath, extended, ); if (!dependencyPath) { return null; } return readCollection( workspacePath, await packageDetails(dependencyPath), options, ); }), ) ) .flat() .filter((c): c is CollectionInfo => Boolean(c)); for (const collection of extendedCollections) { if (collectionMap.has(collection.name)) { continue; } collectionMap.set(collection.name, collection); } } return Array.from(collectionMap.values()); } function readCollectionGenerator( collectionName: string, collectionSchemaName: string, collectionJson: any, ): Generator | undefined { try { let generatorType: GeneratorType; switch (collectionJson['x-type']) { case 'application': generatorType = GeneratorType.Application; break; case 'library': generatorType = GeneratorType.Library; break; default: generatorType = GeneratorType.Other; break; } return { name: collectionSchemaName, collection: collectionName, description: collectionJson.description || '', aliases: collectionJson.aliases ?? [], type: generatorType, }; } catch (e) { console.error(e); console.error( `Invalid package.json for schematic ${collectionName}:${collectionSchemaName}`, ); } } async function resolveDelegatedExecutor( delegatedExecutor: string, workspacePath: string, ) { const [pkgName, executor] = delegatedExecutor.split(':'); const dependencyPath = await workspaceDependencyPath(workspacePath, pkgName); if (!dependencyPath) { return null; } const { packageJson: { builders, executors }, packagePath, } = await packageDetails(dependencyPath); const collection = await readJsonFile(executors || builders, packagePath); if (!collection.json?.[executor]) { return null; } if (typeof collection.json[executor] === 'string') { return resolveDelegatedExecutor(collection.json[executor], workspacePath); } return collection.json[executor]; } /** * Checks to see if the collection is usable within Nx Console. * @param name * @param s * @returns */ function canUse( name: string, s: { hidden: boolean; private: boolean; schema: string; extends: boolean; 'x-deprecated'?: string; }, includeHiddenCollections = false, includeNgAddCollection = false, ): boolean { return ( (!s.hidden || includeHiddenCollections) && !s.private && !s.extends && !s['x-deprecated'] && (name !== 'ng-add' || includeNgAddCollection) ); } function collectionNameWithType(name: string, type: 'generator' | 'executor') { return `${name}-${type}`; } function formatCollectionPath( collectionPath: string, jsonFilePath: string, path: string, ): string { return formatPath(resolve(collectionPath, dirname(jsonFilePath), path)); } function formatPath(path: string): string { if (platform() === 'win32') { return `file:///${path.replace(/\\/g, '/')}`; } return path; } /** * Discovers export-based secondary entry points and creates virtual collections for them. * For example, if @myorg/nx-plugin has exports: {"./adapters": "./src/adapters/index.js"} * and there are generators in src/adapters/generators/, this creates a virtual collection * with the name "@myorg/nx-plugin/adapters". */ async function getExportBasedSecondaryEntryPoints(collection: { packagePath: string; packageName: string; packageJson: any; }): Promise<{ packagePath: string; packageName: string; packageJson: any }[]> { const secondaryEntryPoints: { packagePath: string; packageName: string; packageJson: any; }[] = []; try { const { packageJson, packageName, packagePath } = collection; // Check if package has generators/executors and exports if ( !( packageJson.generators || packageJson.schematics || packageJson.executors || packageJson.builders ) ) { return secondaryEntryPoints; } if (!packageJson.exports || typeof packageJson.exports !== 'object') { return secondaryEntryPoints; } // For each export path, check if it corresponds to generators/executors for (const [exportPath, exportTarget] of Object.entries( packageJson.exports, )) { if (typeof exportTarget === 'string' && exportPath.startsWith('./')) { const subpackageName = exportPath.slice(2); // Remove './' // Check if the export path corresponds to generator files const targetPath = resolve(packagePath, exportTarget); const generatorsPath = join(dirname(targetPath), 'generators'); if (await directoryExists(generatorsPath)) { // Create a virtual secondary entry point collection secondaryEntryPoints.push({ packagePath: packagePath, // Same physical path packageName: `${packageName}/${subpackageName}`, // Virtual collection name packageJson: { ...packageJson, // Mark this as a secondary entry point for filtering generators __secondaryEntryPoint: subpackageName, }, }); } } } } catch { // Ignore errors when scanning for export-based entry points } return secondaryEntryPoints; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nrwl/nx-console'

If you have feedback or need assistance with the MCP directory API, please join our Discord server