From 43386b4c37dd18a5920f22a685638e10ca50a72b Mon Sep 17 00:00:00 2001 From: Jonathan Cammisuli Date: Sat, 16 Oct 2021 17:59:59 -0400 Subject: [PATCH 1/7] perf: asynchronously retrieve all collection info (schematics, generators, builders and executors) --- apps/vscode/src/main.ts | 15 +- libs/schema/src/index.ts | 6 +- libs/server/src/index.ts | 5 +- libs/server/src/lib/select-generator.ts | 37 ++- libs/server/src/lib/utils/get-executors.ts | 14 + libs/server/src/lib/utils/get-generators.ts | 175 ++++++++++++ libs/server/src/lib/utils/read-collections.ts | 167 ++++++++++++ .../lib/utils/read-generator-collections.ts | 256 ------------------ libs/server/src/lib/utils/read-projects.ts | 6 +- libs/server/src/lib/utils/utils.ts | 48 ++-- .../json-schema/src/lib/get-all-executors.ts | 109 -------- .../src/lib/project-json-schema.ts | 25 +- .../src/lib/workspace-json-schema.ts | 21 +- .../nx-workspace/src/lib/get-nx-config.ts | 2 +- .../src/lib/get-nx-workspace-config.ts | 2 +- .../nx-workspace/src/lib/get-raw-workspace.ts | 6 +- .../src/lib/reveal-workspace-json.ts | 4 +- .../src/lib/workspace-codelens-provider.ts | 2 +- .../vscode/tasks/src/lib/cli-task-commands.ts | 2 +- .../src/lib/get-task-execution-schema.ts | 1 + 20 files changed, 445 insertions(+), 458 deletions(-) create mode 100644 libs/server/src/lib/utils/get-executors.ts create mode 100644 libs/server/src/lib/utils/get-generators.ts create mode 100644 libs/server/src/lib/utils/read-collections.ts delete mode 100644 libs/server/src/lib/utils/read-generator-collections.ts delete mode 100644 libs/vscode/json-schema/src/lib/get-all-executors.ts diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index 8c1f076134..b9cd1b78b6 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -21,7 +21,7 @@ import { getOutputChannel, getTelemetry, initTelemetry, - readAllGeneratorCollections, + getGenerators, teardownTelemetry, watchFile, } from '@nx-console/server'; @@ -291,20 +291,19 @@ async function setApplicationAndLibraryContext(workspaceJsonPath: string) { ), ]); - const generatorCollections = await readAllGeneratorCollections( - workspaceJsonPath - ); + const generatorCollections = await getGenerators(workspaceJsonPath, 'nx'); + let hasApplicationGenerators = false; let hasLibraryGenerators = false; generatorCollections.forEach((generatorCollection) => { - generatorCollection.generators.forEach((generator) => { - if (generator.type === 'application') { + if (generatorCollection.data) { + if (generatorCollection.data.type === 'application') { hasApplicationGenerators = true; - } else if (generator.type === 'library') { + } else if (generatorCollection.data.type === 'library') { hasLibraryGenerators = true; } - }); + } }); commands.executeCommand( diff --git a/libs/schema/src/index.ts b/libs/schema/src/index.ts index edb4badccf..9929750817 100644 --- a/libs/schema/src/index.ts +++ b/libs/schema/src/index.ts @@ -47,9 +47,11 @@ export interface TaskExecutionSchema { contextValues?: Record; } -export interface GeneratorCollection { +export interface CollectionInfo { name: string; - generators: Generator[]; + path: string; + type: 'executor' | 'generator'; + data?: Generator; } export enum GeneratorType { diff --git a/libs/server/src/index.ts b/libs/server/src/index.ts index b254f23f7b..3e814927a5 100644 --- a/libs/server/src/index.ts +++ b/libs/server/src/index.ts @@ -5,7 +5,9 @@ export * from './lib/select-generator'; export * from './lib/telemetry'; export * from './lib/utils/output-channel'; export * from './lib/utils/read-projects'; -export * from './lib/utils/read-generator-collections'; +export * from './lib/utils/get-generators'; +export * from './lib/utils/get-executors'; +export * from './lib/utils/read-collections'; export { fileExistsSync, readAndParseJson, @@ -13,6 +15,5 @@ export { cacheJson, clearJsonCache, toWorkspaceFormat, - listOfUnnestedNpmPackages, } from './lib/utils/utils'; export { watchFile } from './lib/utils/watch-file'; diff --git a/libs/server/src/lib/select-generator.ts b/libs/server/src/lib/select-generator.ts index d099e4875c..f70c522991 100644 --- a/libs/server/src/lib/select-generator.ts +++ b/libs/server/src/lib/select-generator.ts @@ -1,13 +1,11 @@ -import { Generator, GeneratorType } from '@nx-console/schema'; +import { CollectionInfo, Generator, GeneratorType } from '@nx-console/schema'; import { TaskExecutionSchema } from '@nx-console/schema'; import { QuickPickItem, window } from 'vscode'; -import { - readAllGeneratorCollections, - readGeneratorOptions, -} from './utils/read-generator-collections'; +import { getGenerators, readGeneratorOptions } from './utils/get-generators'; export async function selectGenerator( workspaceJsonPath: string, + workspaceType: 'nx' | 'ng', generatorType?: GeneratorType ): Promise { interface GenerateQuickPickItem extends QuickPickItem { @@ -15,28 +13,27 @@ export async function selectGenerator( generator: Generator; } - let generators = (await readAllGeneratorCollections(workspaceJsonPath)) - .filter((c) => c && c.generators.length) - .map((c): GenerateQuickPickItem[] => - c.generators.map( - (s: Generator): GenerateQuickPickItem => ({ - description: s.description, - label: `${c.name} - ${s.name}`, - collectionName: c.name, - generator: s, - }) - ) - ) - .flat(); + const generators = await getGenerators(workspaceJsonPath, workspaceType); + let generatorsQuickPicks = generators + .filter((c) => !!c.data) + .map((c): GenerateQuickPickItem => { + const generatorData = c.data!; + return { + description: generatorData.description, + label: `${generatorData.collection} - ${generatorData.name}`, + collectionName: generatorData.collection, + generator: generatorData, + }; + }); if (generatorType) { - generators = generators.filter((generator) => { + generatorsQuickPicks = generatorsQuickPicks.filter((generator) => { return generator.generator.type === generatorType; }); } if (generators) { - const selection = await window.showQuickPick(generators); + const selection = await window.showQuickPick(generatorsQuickPicks); if (selection) { const options = selection.generator.options || diff --git a/libs/server/src/lib/utils/get-executors.ts b/libs/server/src/lib/utils/get-executors.ts new file mode 100644 index 0000000000..8fc6a85029 --- /dev/null +++ b/libs/server/src/lib/utils/get-executors.ts @@ -0,0 +1,14 @@ +import { CollectionInfo } from '@nx-console/schema'; +import { readCollectionsFromNodeModules } from './read-collections'; + +export async function getExecutors( + workspaceJsonPath: string, + clearPackageJsonCache: boolean +): Promise { + return ( + await readCollectionsFromNodeModules( + workspaceJsonPath, + clearPackageJsonCache + ) + ).filter((collection) => collection.type === 'executor'); +} diff --git a/libs/server/src/lib/utils/get-generators.ts b/libs/server/src/lib/utils/get-generators.ts new file mode 100644 index 0000000000..b621b1dc37 --- /dev/null +++ b/libs/server/src/lib/utils/get-generators.ts @@ -0,0 +1,175 @@ +import { + Option, + Generator, + CollectionInfo, + GeneratorType, +} from '@nx-console/schema'; +import { basename, dirname, join } from 'path'; + +import { + directoryExists, + fileExistsSync, + listFiles, + normalizeSchema, + readAndCacheJsonFile, + toWorkspaceFormat, +} from './utils'; +import { + getCollectionInfo, + readCollectionsFromNodeModules, +} from './read-collections'; + +export async function getGenerators( + workspaceJsonPath: string, + workspaceType: 'nx' | 'ng' +): Promise { + const basedir = join(workspaceJsonPath, '..'); + const collections = await readCollectionsFromNodeModules( + workspaceJsonPath, + false + ); + let generatorCollections = collections.filter( + (collection) => collection.type === 'generator' + ); + + generatorCollections = [ + ...generatorCollections, + ...(await checkAndReadWorkspaceCollection( + basedir, + join('tools', 'schematics'), + workspaceType + )), + ...(await checkAndReadWorkspaceCollection( + basedir, + join('tools', 'generators'), + workspaceType + )), + ]; + return generatorCollections.filter( + (collection): collection is CollectionInfo => !!collection.data + ); +} + +async function checkAndReadWorkspaceCollection( + basedir: string, + workspaceGeneratorsPath: string, + workspaceType: 'nx' | 'ng' +) { + if (await directoryExists(join(basedir, workspaceGeneratorsPath))) { + const collection = await readWorkspaceGeneratorsCollection( + basedir, + workspaceGeneratorsPath, + workspaceType + ); + return collection; + } + return Promise.resolve([]); +} + +async function readWorkspaceJsonDefaults( + workspaceJsonPath: string +): Promise { + const workspaceJson = await readAndCacheJsonFile(workspaceJsonPath); + const defaults = toWorkspaceFormat(workspaceJson.json).generators || {}; + const collectionDefaults = Object.keys(defaults).reduce( + (collectionDefaultsMap: any, key) => { + if (key.includes(':')) { + const [collectionName, generatorName] = key.split(':'); + if (!collectionDefaultsMap[collectionName]) { + collectionDefaultsMap[collectionName] = {}; + } + collectionDefaultsMap[collectionName][generatorName] = defaults[key]; + } else { + const collectionName = key; + if (!collectionDefaultsMap[collectionName]) { + collectionDefaultsMap[collectionName] = {}; + } + Object.keys(defaults[collectionName]).forEach((generatorName) => { + collectionDefaultsMap[collectionName][generatorName] = + defaults[collectionName][generatorName]; + }); + } + return collectionDefaultsMap; + }, + {} + ); + return collectionDefaults; +} + +async function readWorkspaceGeneratorsCollection( + basedir: string, + workspaceGeneratorsPath: string, + workspaceType: 'nx' | 'ng' +): Promise { + const collectionDir = join(basedir, workspaceGeneratorsPath); + const collectionName = + workspaceType === 'nx' ? 'workspace-generator' : 'workspace-schematic'; + const collectionPath = join(collectionDir, 'collection.json'); + if (fileExistsSync(collectionPath)) { + const collection = await readAndCacheJsonFile( + 'collection.json', + collectionDir + ); + + return getCollectionInfo( + collectionName, + collectionPath, + {}, + collection.json + ); + } else { + return await Promise.all( + listFiles(collectionDir) + .filter((f) => basename(f) === 'schema.json') + .map(async (f) => { + const schemaJson = await readAndCacheJsonFile(f, ''); + return { + name: collectionName, + type: 'generator', + path: collectionDir, + data: { + name: schemaJson.json.id || schemaJson.json.$id, + collection: collectionName, + options: await normalizeSchema(schemaJson.json), + description: '', + type: GeneratorType.Other, + }, + } as CollectionInfo; + }) + ); + } +} + +export async function readGeneratorOptions( + workspaceJsonPath: string, + collectionName: string, + generatorName: string +): Promise { + const basedir = join(workspaceJsonPath, '..'); + const nodeModulesDir = join(basedir, 'node_modules'); + const collectionPackageJson = await readAndCacheJsonFile( + join(collectionName, 'package.json'), + nodeModulesDir + ); + const collectionJson = await readAndCacheJsonFile( + collectionPackageJson.json.schematics || + collectionPackageJson.json.generators, + dirname(collectionPackageJson.path) + ); + const generators = Object.assign( + {}, + collectionJson.json.schematics, + collectionJson.json.generators + ); + + const generatorSchema = await readAndCacheJsonFile( + generators[generatorName].schema, + dirname(collectionJson.path) + ); + const workspaceDefaults = await readWorkspaceJsonDefaults(workspaceJsonPath); + const defaults = + workspaceDefaults && + workspaceDefaults[collectionName] && + workspaceDefaults[collectionName][generatorName]; + return await normalizeSchema(generatorSchema.json, defaults); +} diff --git a/libs/server/src/lib/utils/read-collections.ts b/libs/server/src/lib/utils/read-collections.ts new file mode 100644 index 0000000000..0eacc2ea08 --- /dev/null +++ b/libs/server/src/lib/utils/read-collections.ts @@ -0,0 +1,167 @@ +import { clearJsonCache, readAndCacheJsonFile } from '@nx-console/server'; +import { platform } from 'os'; +import { dirname, join } from 'path'; +import { CollectionInfo, Generator, GeneratorType } from '@nx-console/schema'; + +export async function readCollectionsFromNodeModules( + workspaceJsonPath: string, + clearPackageJsonCache: boolean +): Promise { + const basedir = dirname(workspaceJsonPath); + const nodeModulesDir = join(basedir, 'node_modules'); + + if (clearPackageJsonCache) { + clearJsonCache('package.json', basedir); + } + + const packageJson = (await readAndCacheJsonFile('package.json', basedir)) + .json; + const packages: { [packageName: string]: string } = { + ...(packageJson.devDependencies || {}), + ...(packageJson.dependencies || {}), + }; + + const collections = await Promise.all( + Object.keys(packages).map(async (p) => { + const json = await readAndCacheJsonFile( + join(p, 'package.json'), + nodeModulesDir + ); + return { + packageName: p, + packageJson: json.json, + }; + }) + ); + + const collectionMap = await Promise.all( + collections.map((c) => readCollections(nodeModulesDir, c.packageName)) + ); + + return collectionMap.flat().filter((c): c is CollectionInfo => Boolean(c)); +} + +export async function readCollections( + basedir: string, + collectionName: string +): Promise { + try { + const packageJson = await readAndCacheJsonFile( + join(collectionName, 'package.json'), + basedir + ); + + const [executorCollections, generatorCollections] = await Promise.all([ + readAndCacheJsonFile( + packageJson.json.executors || packageJson.json.builders, + dirname(packageJson.path) + ), + readAndCacheJsonFile( + packageJson.json.generators || packageJson.json.schematics, + dirname(packageJson.path) + ), + ]); + + return getCollectionInfo( + collectionName, + packageJson.path, + executorCollections.json, + generatorCollections.json + ); + } catch (e) { + return null; + } +} + +export function getCollectionInfo( + collectionName: string, + path: string, + executorCollectionJson: any, + generatorCollectionJson: any +): CollectionInfo[] { + const baseDir = dirname(path); + + const collection: CollectionInfo[] = []; + + const buildCollectionInfo = ( + name: string, + value: any, + type: 'executor' | 'generator' + ): CollectionInfo => { + let path = ''; + if (platform() === 'win32') { + path = `file:///${join(baseDir, value.schema).replace(/\\/g, '/')}`; + } else { + path = join(baseDir, value.schema); + } + + return { + name: `${collectionName}:${name}`, + type, + path, + }; + }; + + for (const [key, value] of Object.entries( + executorCollectionJson.executors || executorCollectionJson.executors || {} + )) { + collection.push(buildCollectionInfo(key, value, 'executor')); + } + + for (const [key, value] of Object.entries( + generatorCollectionJson.generators || + generatorCollectionJson.schematics || + {} + )) { + try { + const collectionInfo = buildCollectionInfo(key, value, 'generator'); + collectionInfo.data = readCollectionGenerator(collectionName, key, value); + collection.push(collectionInfo); + } catch (e) { + // noop - generator is invalid + } + } + + return collection; +} + +function readCollectionGenerator( + collectionName: string, + collectionSchemaName: string, + collectionJson: any +): Generator | undefined { + try { + if (canAdd(collectionSchemaName, collectionJson)) { + 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 || '', + type: generatorType, + }; + } + } catch (e) { + console.error(e); + console.error( + `Invalid package.json for schematic ${collectionName}:${collectionSchemaName}` + ); + } +} + +function canAdd( + name: string, + s: { hidden: boolean; private: boolean; schema: string; extends: boolean } +): boolean { + return !s.hidden && !s.private && !s.extends && name !== 'ng-add'; +} diff --git a/libs/server/src/lib/utils/read-generator-collections.ts b/libs/server/src/lib/utils/read-generator-collections.ts deleted file mode 100644 index ff5bea51f7..0000000000 --- a/libs/server/src/lib/utils/read-generator-collections.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { - Option, - Generator, - GeneratorCollection, - GeneratorType, -} from '@nx-console/schema'; -import { basename, dirname, join } from 'path'; - -import { - directoryExists, - fileExistsSync, - listFiles, - listOfUnnestedNpmPackages, - normalizeSchema, - readAndCacheJsonFile, - toWorkspaceFormat, -} from './utils'; - -export async function readAllGeneratorCollections( - workspaceJsonPath: string -): Promise { - const basedir = join(workspaceJsonPath, '..'); - let collections = await readGeneratorCollectionsFromNodeModules( - workspaceJsonPath - ); - collections = [ - ...collections, - ...(await checkAndReadWorkspaceCollection( - basedir, - join('tools', 'schematics') - )), - ...(await checkAndReadWorkspaceCollection( - basedir, - join('tools', 'generators') - )), - ]; - return collections.filter( - (collection): collection is GeneratorCollection => - collection && collection.generators.length > 0 - ); -} - -async function checkAndReadWorkspaceCollection( - basedir: string, - workspaceGeneratorsPath: string -) { - if (directoryExists(join(basedir, workspaceGeneratorsPath))) { - return readWorkspaceGeneratorsCollection( - basedir, - workspaceGeneratorsPath - ).then((val) => [val]); - } - return Promise.resolve([]); -} - -function readWorkspaceJsonDefaults(workspaceJsonPath: string): any { - // TODO(Cammisuli): Change this to use getNxConfig to support nx 13 - const defaults = - (toWorkspaceFormat(readAndCacheJsonFile(workspaceJsonPath).json) as any) - .generators || {}; - - const collectionDefaults = Object.keys(defaults).reduce( - (collectionDefaultsMap: any, key) => { - if (key.includes(':')) { - const [collectionName, generatorName] = key.split(':'); - if (!collectionDefaultsMap[collectionName]) { - collectionDefaultsMap[collectionName] = {}; - } - collectionDefaultsMap[collectionName][generatorName] = defaults[key]; - } else { - const collectionName = key; - if (!collectionDefaultsMap[collectionName]) { - collectionDefaultsMap[collectionName] = {}; - } - Object.keys(defaults[collectionName]).forEach((generatorName) => { - collectionDefaultsMap[collectionName][generatorName] = - defaults[collectionName][generatorName]; - }); - } - return collectionDefaultsMap; - }, - {} - ); - return collectionDefaults; -} - -async function readGeneratorCollectionsFromNodeModules( - workspaceJsonPath: string -): Promise { - const basedir = join(workspaceJsonPath, '..'); - const nodeModulesDir = join(basedir, 'node_modules'); - const packages = listOfUnnestedNpmPackages(nodeModulesDir); - const generatorCollections = packages.filter((p) => { - try { - const packageJson = readAndCacheJsonFile( - join(p, 'package.json'), - nodeModulesDir - ).json; - return !!(packageJson.schematics || packageJson.generators); - } catch (e) { - if ( - e.message && - (e.message.indexOf('no such file') > -1 || - e.message.indexOf('not a directory') > -1) - ) { - return false; - } else { - throw e; - } - } - }); - - return ( - await Promise.all( - generatorCollections.map((c) => readCollection(nodeModulesDir, c)) - ) - ).filter((c): c is GeneratorCollection => Boolean(c)); -} - -async function readWorkspaceGeneratorsCollection( - basedir: string, - workspaceGeneratorsPath: string -): Promise { - const collectionDir = join(basedir, workspaceGeneratorsPath); - const collectionName = 'workspace-schematic'; - if (fileExistsSync(join(collectionDir, 'collection.json'))) { - const collection = readAndCacheJsonFile('collection.json', collectionDir); - - return readCollectionGenerators(collectionName, collection.json); - } else { - const generators: Generator[] = await Promise.all( - listFiles(collectionDir) - .filter((f) => basename(f) === 'schema.json') - .map(async (f) => { - const schemaJson = readAndCacheJsonFile(f, ''); - return { - name: schemaJson.json.id || schemaJson.json.$id, - collection: collectionName, - options: await normalizeSchema(schemaJson.json), - description: '', - type: GeneratorType.Other, - }; - }) - ); - return { name: collectionName, generators }; - } -} - -async function readCollection( - basedir: string, - collectionName: string -): Promise { - try { - const packageJson = readAndCacheJsonFile( - join(collectionName, 'package.json'), - basedir - ); - const collection = readAndCacheJsonFile( - packageJson.json.schematics || packageJson.json.generators, - dirname(packageJson.path) - ); - return readCollectionGenerators(collectionName, collection.json); - } catch (e) { - // this happens when package is misconfigured. We decided to ignore such a case. - return null; - } -} - -function readCollectionGenerators( - collectionName: string, - collectionJson: any -): GeneratorCollection { - const generators = new Set(); - - try { - Object.entries( - Object.assign({}, collectionJson.schematics, collectionJson.generators) - ).forEach(([k, v]: [any, any]) => { - try { - if (canAdd(k, v)) { - let generatorType: GeneratorType; - switch (v['x-type']) { - case 'application': - generatorType = GeneratorType.Application; - break; - case 'library': - generatorType = GeneratorType.Library; - break; - default: - generatorType = GeneratorType.Other; - break; - } - generators.add({ - name: k, - collection: collectionName, - description: v.description || '', - type: generatorType, - }); - } - } catch (e) { - console.error(e); - console.error( - `Invalid package.json for schematic ${collectionName}:${k}` - ); - } - }); - } catch (e) { - console.error(e); - console.error(`Invalid package.json for schematic ${collectionName}`); - } - return { - name: collectionName, - generators: Array.from(generators), - }; -} - -export async function readGeneratorOptions( - workspaceJsonPath: string, - collectionName: string, - generatorName: string -): Promise { - const basedir = join(workspaceJsonPath, '..'); - const nodeModulesDir = join(basedir, 'node_modules'); - const collectionPackageJson = readAndCacheJsonFile( - join(collectionName, 'package.json'), - nodeModulesDir - ); - const collectionJson = readAndCacheJsonFile( - collectionPackageJson.json.schematics || - collectionPackageJson.json.generators, - dirname(collectionPackageJson.path) - ); - const generators = Object.assign( - {}, - collectionJson.json.schematics, - collectionJson.json.generators - ); - - const generatorSchema = readAndCacheJsonFile( - generators[generatorName].schema, - dirname(collectionJson.path) - ); - const workspaceDefaults = readWorkspaceJsonDefaults(workspaceJsonPath); - const defaults = - workspaceDefaults && - workspaceDefaults[collectionName] && - workspaceDefaults[collectionName][generatorName]; - return await normalizeSchema(generatorSchema.json, defaults); -} - -function canAdd( - name: string, - s: { hidden: boolean; private: boolean; schema: string; extends: boolean } -): boolean { - return !s.hidden && !s.private && !s.extends && name !== 'ng-add'; -} diff --git a/libs/server/src/lib/utils/read-projects.ts b/libs/server/src/lib/utils/read-projects.ts index 2c3763151a..0f6c529a43 100644 --- a/libs/server/src/lib/utils/read-projects.ts +++ b/libs/server/src/lib/utils/read-projects.ts @@ -59,20 +59,20 @@ export async function readBuilderSchema( projectDefaults?: { [name: string]: string } ): Promise { const [npmPackage, builderName] = builder.split(':'); - const packageJson = readAndCacheJsonFile( + const packageJson = await readAndCacheJsonFile( path.join(npmPackage, 'package.json'), path.join(basedir, 'node_modules') ); const b = packageJson.json.builders || packageJson.json.executors; const buildersPath = b.startsWith('.') ? b : `./${b}`; - const buildersJson = readAndCacheJsonFile( + const buildersJson = await readAndCacheJsonFile( buildersPath, path.dirname(packageJson.path) ); const builderDef = (buildersJson.json.builders || buildersJson.json.executors)[builderName]; - const builderSchema = readAndCacheJsonFile( + const builderSchema = await readAndCacheJsonFile( builderDef.schema, path.dirname(buildersJson.path) ); diff --git a/libs/server/src/lib/utils/utils.ts b/libs/server/src/lib/utils/utils.ts index 1fc207ba76..3d3e3f3f8b 100644 --- a/libs/server/src/lib/utils/utils.ts +++ b/libs/server/src/lib/utils/utils.ts @@ -11,7 +11,8 @@ import { OptionItemLabelValue, XPrompt, } from '@nx-console/schema'; -import { existsSync, readdirSync, readFileSync, statSync } from 'fs'; +import { readdirSync, statSync } from 'fs'; +import { readFile, stat } from 'fs/promises'; import { parse as parseJson, ParseError, @@ -38,24 +39,6 @@ const IMPORTANT_FIELD_NAMES = [ ]; const IMPORTANT_FIELDS_SET = new Set(IMPORTANT_FIELD_NAMES); -export function listOfUnnestedNpmPackages(nodeModulesDir: string): string[] { - const res: string[] = []; - if (!existsSync(nodeModulesDir)) { - return res; - } - - readdirSync(nodeModulesDir).forEach((npmPackageOrScope) => { - if (npmPackageOrScope.startsWith('@')) { - readdirSync(path.join(nodeModulesDir, npmPackageOrScope)).forEach((p) => { - res.push(`${npmPackageOrScope}/${p}`); - }); - } else { - res.push(npmPackageOrScope); - } - }); - return res; -} - export function listFiles(dirName: string): string[] { // TODO use .gitignore to skip files if (dirName.indexOf('node_modules') > -1) return []; @@ -83,9 +66,9 @@ export function listFiles(dirName: string): string[] { return res; } -export function directoryExists(filePath: string): boolean { +export async function directoryExists(filePath: string): Promise { try { - return statSync(filePath).isDirectory(); + return (await stat(filePath)).isDirectory(); } catch { return false; } @@ -99,8 +82,8 @@ export function fileExistsSync(filePath: string): boolean { } } -export function readAndParseJson(filePath: string) { - const content = readFileSync(filePath, 'utf-8'); +export async function readAndParseJson(filePath: string) { + const content = await readFile(filePath, 'utf-8'); try { return JSON.parse(content); } catch { @@ -147,14 +130,21 @@ export function cacheJson(filePath: string, basedir = '', content?: any) { }; } -export function readAndCacheJsonFile( - filePath: string, +export async function readAndCacheJsonFile( + filePath: string | undefined, basedir = '' -): { path: string; json: any } { - const fullFilePath = path.join(basedir, filePath); +): Promise<{ path: string; json: any }> { + if (!filePath) { + return { + path: '', + json: {}, + }; + } - if (fileContents[fullFilePath] || existsSync(fullFilePath)) { - fileContents[fullFilePath] ||= readAndParseJson(fullFilePath); + const fullFilePath = path.join(basedir, filePath); + const stats = await stat(fullFilePath); + if (fileContents[fullFilePath] || stats.isFile()) { + fileContents[fullFilePath] ||= await readAndParseJson(fullFilePath); return { path: fullFilePath, diff --git a/libs/vscode/json-schema/src/lib/get-all-executors.ts b/libs/vscode/json-schema/src/lib/get-all-executors.ts deleted file mode 100644 index 26191bbac2..0000000000 --- a/libs/vscode/json-schema/src/lib/get-all-executors.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { clearJsonCache, readAndCacheJsonFile } from '@nx-console/server'; -import { dirname, join } from 'path'; -import { platform } from 'os'; - -export interface ExecutorInfo { - name: string; - path: string; -} - -export function getAllExecutors( - workspaceJsonPath: string, - clearPackageJsonCache: boolean -): ExecutorInfo[] { - return readExecutorCollectionsFromNodeModules( - workspaceJsonPath, - clearPackageJsonCache - ); -} - -function readExecutorCollectionsFromNodeModules( - workspaceJsonPath: string, - clearPackageJsonCache: boolean -): ExecutorInfo[] { - const basedir = dirname(workspaceJsonPath); - const nodeModulesDir = join(basedir, 'node_modules'); - - if (clearPackageJsonCache) { - clearJsonCache('package.json', basedir); - } - const packageJson = readAndCacheJsonFile('package.json', basedir).json; - const packages: { [packageName: string]: string } = { - ...(packageJson.devDependencies || {}), - ...(packageJson.dependencies || {}), - }; - const executorCollections = Object.keys(packages).filter((p) => { - try { - const packageJson = readAndCacheJsonFile( - join(p, 'package.json'), - nodeModulesDir - ).json; - // TODO: to add support for schematics, we can change this to include schematics/generators - return !!(packageJson.builders || packageJson.executors); - } catch (e) { - if ( - e.message && - (e.message.indexOf('no such file') > -1 || - e.message.indexOf('not a directory') > -1) - ) { - return false; - } else { - throw e; - } - } - }); - - return executorCollections - .map((c) => readCollections(nodeModulesDir, c)) - .flat() - .filter((c): c is ExecutorInfo => Boolean(c)); -} - -function readCollections( - basedir: string, - collectionName: string -): ExecutorInfo[] | null { - try { - const packageJson = readAndCacheJsonFile( - join(collectionName, 'package.json'), - basedir - ); - - const collection = readAndCacheJsonFile( - packageJson.json.builders || packageJson.json.executors, - dirname(packageJson.path) - ); - - return getBuilderPaths(collectionName, collection.path, collection.json); - } catch (e) { - return null; - } -} - -function getBuilderPaths( - collectionName: string, - path: string, - json: any -): ExecutorInfo[] { - const baseDir = dirname(path); - - const builders: ExecutorInfo[] = []; - for (const [key, value] of Object.entries( - json.builders || json.executors - )) { - let path = ''; - if (platform() === 'win32') { - path = `file:///${join(baseDir, value.schema).replace(/\\/g, '/')}`; - } else { - path = join(baseDir, value.schema); - } - - builders.push({ - name: `${collectionName}:${key}`, - path, - }); - } - - return builders; -} diff --git a/libs/vscode/json-schema/src/lib/project-json-schema.ts b/libs/vscode/json-schema/src/lib/project-json-schema.ts index 49372fae3c..d5dc57bf11 100644 --- a/libs/vscode/json-schema/src/lib/project-json-schema.ts +++ b/libs/vscode/json-schema/src/lib/project-json-schema.ts @@ -1,8 +1,8 @@ -import { watchFile } from '@nx-console/server'; +import { CollectionInfo } from '@nx-console/schema'; +import { getExecutors, watchFile } from '@nx-console/server'; import { WorkspaceConfigurationStore } from '@nx-console/vscode/configuration'; import { dirname, join } from 'path'; import * as vscode from 'vscode'; -import { ExecutorInfo, getAllExecutors } from './get-all-executors'; let FILE_WATCHER: vscode.FileSystemWatcher; @@ -32,13 +32,16 @@ export class ProjectJsonSchema { this.setupSchema(workspacePath, context.extensionUri); } - setupSchema( + async setupSchema( workspacePath: string, extensionUri: vscode.Uri, clearPackageJsonCache = false ) { const filePath = vscode.Uri.joinPath(extensionUri, 'project-schema.json'); - const collections = getAllExecutors(workspacePath, clearPackageJsonCache); + const collections = await getExecutors( + workspacePath, + clearPackageJsonCache + ); const contents = getProjectJsonSchema(collections); vscode.workspace.fs.writeFile( filePath, @@ -47,14 +50,14 @@ export class ProjectJsonSchema { } } -function getProjectJsonSchema(collections: ExecutorInfo[]) { +function getProjectJsonSchema(collections: CollectionInfo[]) { const [builders, executors] = createBuildersAndExecutorsSchema(collections); const contents = createJsonSchema(builders, executors); return contents; } function createBuildersAndExecutorsSchema( - collections: ExecutorInfo[] + collections: CollectionInfo[] ): [string, string] { const builders = collections .map( @@ -65,10 +68,10 @@ function createBuildersAndExecutorsSchema( "required": ["builder"] }, "then": { - "properties": { + "properties": { "options": { "$ref": "${collection.path}" - }, + }, "configurations": { "additionalProperties": { "$ref": "${collection.path}", @@ -85,13 +88,13 @@ function createBuildersAndExecutorsSchema( const executors = collections .map( (collection) => ` -{ +{ "if": { "properties": { "executor": { "const": "${collection.name}" } }, "required": ["executor"] }, "then": { - "properties": { + "properties": { "options": { "$ref": "${collection.path}" }, @@ -138,7 +141,7 @@ function createJsonSchema(builders: string, executors: string) { } }, "allOf": [ - ${executors} + ${executors} ] } } diff --git a/libs/vscode/json-schema/src/lib/workspace-json-schema.ts b/libs/vscode/json-schema/src/lib/workspace-json-schema.ts index ea938e2b17..5edacf1c05 100644 --- a/libs/vscode/json-schema/src/lib/workspace-json-schema.ts +++ b/libs/vscode/json-schema/src/lib/workspace-json-schema.ts @@ -1,8 +1,8 @@ -import { watchFile } from '@nx-console/server'; +import { CollectionInfo } from '@nx-console/schema'; +import { getExecutors, watchFile } from '@nx-console/server'; import { WorkspaceConfigurationStore } from '@nx-console/vscode/configuration'; import { dirname, join } from 'path'; import * as vscode from 'vscode'; -import { ExecutorInfo, getAllExecutors } from './get-all-executors'; let FILE_WATCHER: vscode.FileSystemWatcher; @@ -32,14 +32,17 @@ export class WorkspaceJsonSchema { this.setupSchema(workspacePath, context.extensionUri); } - setupSchema( + async setupSchema( workspacePath: string, extensionUri: vscode.Uri, clearPackageJsonCache = false ) { const filePath = vscode.Uri.joinPath(extensionUri, 'workspace-schema.json'); - const collections = getAllExecutors(workspacePath, clearPackageJsonCache); - const contents = getWorkspaceJsonSchema(collections); + const collections = await getExecutors( + workspacePath, + clearPackageJsonCache + ); + const contents = await getWorkspaceJsonSchema(collections); vscode.workspace.fs.writeFile( filePath, new Uint8Array(Buffer.from(contents, 'utf8')) @@ -47,14 +50,14 @@ export class WorkspaceJsonSchema { } } -function getWorkspaceJsonSchema(collections: ExecutorInfo[]) { +function getWorkspaceJsonSchema(collections: CollectionInfo[]) { const [builders, executors] = createBuildersAndExecutorsSchema(collections); const contents = createJsonSchema(builders, executors); return contents; } function createBuildersAndExecutorsSchema( - collections: ExecutorInfo[] + collections: CollectionInfo[] ): [string, string] { const builders = collections .map( @@ -131,7 +134,7 @@ function createJsonSchema(builders: string, executors: string) { }, "then": { "description": "Read more about this workspace file at https://nx.dev/latest/angular/getting-started/configuration", - "properties": { + "properties": { "projects": { "type": "object", "additionalProperties": { @@ -157,7 +160,7 @@ function createJsonSchema(builders: string, executors: string) { } }, "allOf": [ - ${builders} + ${builders} ] } } diff --git a/libs/vscode/nx-workspace/src/lib/get-nx-config.ts b/libs/vscode/nx-workspace/src/lib/get-nx-config.ts index f603939220..9b4826132a 100644 --- a/libs/vscode/nx-workspace/src/lib/get-nx-config.ts +++ b/libs/vscode/nx-workspace/src/lib/get-nx-config.ts @@ -18,6 +18,6 @@ export async function getNxConfig( } return cachedNxJson; } catch (e) { - return readAndCacheJsonFile('nx.json', baseDir).json; + return (await readAndCacheJsonFile('nx.json', baseDir)).json; } } diff --git a/libs/vscode/nx-workspace/src/lib/get-nx-workspace-config.ts b/libs/vscode/nx-workspace/src/lib/get-nx-workspace-config.ts index f5cbc534d3..ac2ae5971c 100644 --- a/libs/vscode/nx-workspace/src/lib/get-nx-workspace-config.ts +++ b/libs/vscode/nx-workspace/src/lib/get-nx-workspace-config.ts @@ -20,7 +20,7 @@ export async function getNxWorkspaceConfig( } return cachedWorkspaceJson; } catch (e) { - return readAndCacheJsonFile(workspaceJsonPath).json; + return (await readAndCacheJsonFile(workspaceJsonPath)).json; } } diff --git a/libs/vscode/nx-workspace/src/lib/get-raw-workspace.ts b/libs/vscode/nx-workspace/src/lib/get-raw-workspace.ts index d18ed6cd66..f636b7ade3 100644 --- a/libs/vscode/nx-workspace/src/lib/get-raw-workspace.ts +++ b/libs/vscode/nx-workspace/src/lib/get-raw-workspace.ts @@ -5,14 +5,14 @@ import { WorkspaceConfigurationStore } from '@nx-console/vscode/configuration'; /** * Get the raw workspace file that hasn't been normalized by nx */ -export function getRawWorkspace() { +export async function getRawWorkspace() { const workspaceJsonPath = WorkspaceConfigurationStore.instance.get( 'nxWorkspaceJsonPath', '' ); - const rawWorkspace = readAndParseJson( + const rawWorkspace = (await readAndParseJson( workspaceJsonPath - ) as WorkspaceJsonConfiguration; + )) as WorkspaceJsonConfiguration; return { rawWorkspace, workspaceJsonPath }; } diff --git a/libs/vscode/nx-workspace/src/lib/reveal-workspace-json.ts b/libs/vscode/nx-workspace/src/lib/reveal-workspace-json.ts index 9ba9839419..8e3fcd7cc7 100644 --- a/libs/vscode/nx-workspace/src/lib/reveal-workspace-json.ts +++ b/libs/vscode/nx-workspace/src/lib/reveal-workspace-json.ts @@ -7,7 +7,7 @@ export async function revealNxProject( projectName: string, target?: { name: string; configuration?: string } ) { - const raw = getRawWorkspace(); + const raw = await getRawWorkspace(); const rawWorkspace = raw.rawWorkspace; let workspaceJsonPath = raw.workspaceJsonPath; @@ -18,7 +18,7 @@ export async function revealNxProject( const workspaceRootDir = dirname(workspaceJsonPath); workspaceJsonPath = join( workspaceRootDir, - (rawWorkspace.projects[projectName] as unknown) as string, + rawWorkspace.projects[projectName] as unknown as string, 'project.json' ); } diff --git a/libs/vscode/nx-workspace/src/lib/workspace-codelens-provider.ts b/libs/vscode/nx-workspace/src/lib/workspace-codelens-provider.ts index ebb4d9f86c..de6b928545 100644 --- a/libs/vscode/nx-workspace/src/lib/workspace-codelens-provider.ts +++ b/libs/vscode/nx-workspace/src/lib/workspace-codelens-provider.ts @@ -58,7 +58,7 @@ export class WorkspaceCodeLensProvider implements CodeLensProvider { * Find the project name of the corresponding project.json file */ if (document.uri.path.endsWith('project.json')) { - const { rawWorkspace } = getRawWorkspace(); + const { rawWorkspace } = await getRawWorkspace(); for (const [key, project] of Object.entries(rawWorkspace.projects)) { if ( typeof project === 'string' && diff --git a/libs/vscode/tasks/src/lib/cli-task-commands.ts b/libs/vscode/tasks/src/lib/cli-task-commands.ts index ea1473eaca..ddcbee985f 100644 --- a/libs/vscode/tasks/src/lib/cli-task-commands.ts +++ b/libs/vscode/tasks/src/lib/cli-task-commands.ts @@ -243,7 +243,7 @@ async function selectGeneratorAndPromptForFlags() { return; } - const selection = await selectGenerator(configurationFilePath); + const selection = await selectGenerator(configurationFilePath, workspaceType); if (!selection) { return; } diff --git a/libs/vscode/webview/src/lib/get-task-execution-schema.ts b/libs/vscode/webview/src/lib/get-task-execution-schema.ts index 25631f1348..82e224c648 100644 --- a/libs/vscode/webview/src/lib/get-task-execution-schema.ts +++ b/libs/vscode/webview/src/lib/get-task-execution-schema.ts @@ -114,6 +114,7 @@ export async function getTaskExecutionSchema( case 'Generate': { const generator = await selectGenerator( cliTaskProvider.getWorkspaceJsonPath(), + workspaceType, generatorType ); From 72738e54bc0a6fe6f189d21907d00c44378358e0 Mon Sep 17 00:00:00 2001 From: Jonathan Cammisuli Date: Fri, 22 Oct 2021 10:32:09 -0400 Subject: [PATCH 2/7] check if executors and generators can be used --- libs/server/src/lib/utils/read-collections.ts | 68 ++++++++++++------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/libs/server/src/lib/utils/read-collections.ts b/libs/server/src/lib/utils/read-collections.ts index 0eacc2ea08..99816c55ee 100644 --- a/libs/server/src/lib/utils/read-collections.ts +++ b/libs/server/src/lib/utils/read-collections.ts @@ -1,7 +1,7 @@ -import { clearJsonCache, readAndCacheJsonFile } from '@nx-console/server'; import { platform } from 'os'; import { dirname, join } from 'path'; import { CollectionInfo, Generator, GeneratorType } from '@nx-console/schema'; +import { clearJsonCache, readAndCacheJsonFile } from './utils'; export async function readCollectionsFromNodeModules( workspaceJsonPath: string, @@ -102,20 +102,32 @@ export function getCollectionInfo( }; }; - for (const [key, value] of Object.entries( + for (const [key, schema] of Object.entries( executorCollectionJson.executors || executorCollectionJson.executors || {} )) { - collection.push(buildCollectionInfo(key, value, 'executor')); + if (!canUse(collectionName, schema)) { + continue; + } + + collection.push(buildCollectionInfo(key, schema, 'executor')); } - for (const [key, value] of Object.entries( + for (const [key, schema] of Object.entries( generatorCollectionJson.generators || generatorCollectionJson.schematics || {} )) { + if (!canUse(collectionName, schema)) { + continue; + } + try { - const collectionInfo = buildCollectionInfo(key, value, 'generator'); - collectionInfo.data = readCollectionGenerator(collectionName, key, value); + const collectionInfo = buildCollectionInfo(key, schema, 'generator'); + collectionInfo.data = readCollectionGenerator( + collectionName, + key, + schema + ); collection.push(collectionInfo); } catch (e) { // noop - generator is invalid @@ -131,26 +143,24 @@ function readCollectionGenerator( collectionJson: any ): Generator | undefined { try { - if (canAdd(collectionSchemaName, collectionJson)) { - 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 || '', - type: generatorType, - }; + 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 || '', + type: generatorType, + }; } catch (e) { console.error(e); console.error( @@ -159,7 +169,13 @@ function readCollectionGenerator( } } -function canAdd( +/** + * 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 } ): boolean { From 843b67aee29da0195318a56a8a984723c77e6a16 Mon Sep 17 00:00:00 2001 From: Jonathan Cammisuli Date: Fri, 22 Oct 2021 14:40:18 -0400 Subject: [PATCH 3/7] read extended generators --- libs/server/src/index.ts | 3 +- libs/server/src/lib/extensions.ts | 319 ------------------ libs/server/src/lib/select-generator.ts | 55 --- libs/server/src/lib/utils/get-generators.ts | 84 +---- libs/server/src/lib/utils/read-collections.ts | 78 ++++- libs/vscode/tasks/src/index.ts | 1 + .../vscode/tasks/src/lib/cli-task-commands.ts | 2 +- libs/vscode/tasks/src/lib/select-generator.ts | 146 ++++++++ .../src/lib/get-task-execution-schema.ts | 2 +- 9 files changed, 221 insertions(+), 469 deletions(-) delete mode 100644 libs/server/src/lib/extensions.ts delete mode 100644 libs/server/src/lib/select-generator.ts create mode 100644 libs/vscode/tasks/src/lib/select-generator.ts diff --git a/libs/server/src/index.ts b/libs/server/src/index.ts index 3e814927a5..51138fe702 100644 --- a/libs/server/src/index.ts +++ b/libs/server/src/index.ts @@ -1,7 +1,5 @@ export * from './lib/abstract-tree-provider'; -export * from './lib/extensions'; export * from './lib/stores'; -export * from './lib/select-generator'; export * from './lib/telemetry'; export * from './lib/utils/output-channel'; export * from './lib/utils/read-projects'; @@ -12,6 +10,7 @@ export { fileExistsSync, readAndParseJson, readAndCacheJsonFile, + normalizeSchema, cacheJson, clearJsonCache, toWorkspaceFormat, diff --git a/libs/server/src/lib/extensions.ts b/libs/server/src/lib/extensions.ts deleted file mode 100644 index f3de15c10c..0000000000 --- a/libs/server/src/lib/extensions.ts +++ /dev/null @@ -1,319 +0,0 @@ -// Based primarily off of https://github.com/JetBrains/intellij-plugins/blob/17ec63d95e875b5e2459d570821401596b678f0a/AngularJS/resources/org/angularjs/cli/ng-packages.json -export const EXTENSIONS: { [key: string]: string } = { - '@a.grisevich/ng-zorro-antd': - 'An enterprise-class UI components based on Ant Design and Angular', - '@alyle/ui': 'Minimal Design, a set of components for Angular', - '@angular-buddies/prettier': - 'Your buddy who knows how to make your code pretty using Prettier.', - '@angular-extensions/model': - 'Angular Model - Simple state management with minimalistic API, one way data flow, multiple model support and immutable data exposed as RxJS Observable.', - '@angular-material-extensions/core': - 'Set of components, directives and services to boost the app development with angular material 2', - '@angular-toolkit/serverless': - 'Angular Universal PWA boilerplate for serverless environment.', - '@angular/cdk': 'Angular Material Component Development Kit', - '@angular/elements': - 'Angular - library for using Angular Components as Custom Elements', - '@angular/fire': 'The official library for Firebase and Angular', - '@angular/material': 'Angular Material', - '@angular/pwa': 'PWA schematics for Angular', - '@augury/schematics': 'Schematics for Augury Labs', - '@azure/ng-deploy': - '@azure/ng-deploy - Deploy Angular apps to Azure using the Angular CLI', - '@balticcode/ngx-hotkeys': 'An Angular module providing hotkey support.', - '@briebug/cypress-schematic': 'Add cypress to an Angular CLI project', - '@briebug/jest-schematic': 'Add jest to an Angular CLI project', - '@briebug/mat-dialog-schematic': - 'A schematic for generating mat dialog components', - '@briebug/ngrx-entity-schematic': - 'An Angular schematic for quickly scaffolding NgRx Entities with actions, effects, reducer, model, service, and passing specs.', - '@cameltec-ng/schematics': 'A set of schematics for Angular', - '@capacitor/angular': 'Schematics for capacitor/angular apps.', - '@clr/angular': 'Angular components for Clarity', - '@css_christianscharr/ngx-build-plus': - 'Extensible Builder for the Angular CLI suitable not only for Angular Elements.', - '@datorama/akita': 'State Management Tailored-Made for JS Applications', - '@datorama/akita-experimental': - 'State Management Tailored-Made for JS Applications', - '@davinkevin/jest': - 'Angular Schematics which add Jest to your original setup', - '@devoto13/angular-fontawesome': 'Angular Fontawesome, an Angular library', - '@dnation/web3': - 'A schematics for build decentralised application with Angular and web3', - '@dx-samples/creative-bootstrap-components': - 'Library with reusable WCH components.', - '@electron-schematics/schematics': 'Schematics specific to Electron', - '@froko/ng-essentials': - 'An essentials schematics for new Angular applications', - '@gngt/core': - 'Gnucoop Angular Toolkit \u003d\u003d\u003d\u003d\u003d\u003d\u003d', - '@ibm-wch-sdk/wrtp': 'A schematics to enable an application for WRTP', - '@ionic/angular': 'Angular specific wrappers for @ionic/core', - '@itrulia/jest-schematic': 'Jest schematics for the @angular/cli', - '@jarmee/schematics': 'A collection of schematics', - '@kai1015/serverless': - 'Angular Universal PWA boilerplate for serverless environment.', - '@kamiazya/ngx-speech-recognition': - 'Angular 5+ speech recognition service (based on browser implementation such as Chrome).', - '@kentan-official/schematics': - 'Schematics for @kentan-official/core automating creation of sketches', - '@mace/prettier-schematics': 'Add Prettier to your Angular CLI projects.', - '@materia/schematics-universal': - 'Add Angular Universal support to your angular cli project', - '@momentum-ui/angular': - 'The Cisco Momentum UI Icons library allows developers to easily incorporate Webex Icons and CSS into any application.', - '@mxth/entity': 'Common utilities for entity reducers', - '@nativescript/schematics': 'Schematics for NativeScript Angular apps.', - '@nebular/theme': '@nebular/theme', - '@netbasal/spectator': 'Angular tests made easy', - '@ng-bootstrap/schematics': - 'ng-bootstrap schematics collection for angular-cli', - '@ng-toolkit/firebug': 'Add Firebug lite to your Angular project.', - '@ng-toolkit/pwa': - 'Extension for @angular/pwa - adds server-side rendering fixes and update mechanism', - '@ng-toolkit/serverless': - 'Angular Universal PWA boilerplate for serverless environment.', - '@ng-toolkit/universal': - 'Adds Angular Universal support for any Angular CLI project', - '@ngqp/core': 'Synchronizing form controls with the URL for Angular', - '@ngrx/data': 'API management for NgRx', - '@ngrx/effects': 'Side effect model for @ngrx/store', - '@ngrx/entity': 'Common utilities for entity reducers', - '@ngrx/router-store': 'Bindings to connect @angular/router to @ngrx/store', - '@ngrx/schematics': 'NgRx Schematics for Angular', - '@ngrx/store': 'RxJS powered Redux for Angular apps', - '@ngrx/store-devtools': 'Developer tools for @ngrx/store', - '@nguniversal/express-engine': - 'Express Engine for running Server Angular Apps', - '@nguniversal/hapi-engine': 'Hapi Engine for running Server Angular Apps', - '@ngx-formly/schematics': - 'ngx-formly is an Angular 2 module which has a Components to help customize and render JavaScript/JSON configured forms. The formly-form Component and the FormlyConfig service are very powerful and bring unmatched maintainability to your application\u0027s form', - '@ngx-i18nsupport/tooling': - 'Schematics to add the tooling to be used with the Angular 2 i18n workflow', - '@ngx-kit/core': 'ngx-kit - core module', - '@ngx-kit/sula': 'Sula — Angular UI components', - '@ngxs/schematics': 'NGXS schematics for Angular', - '@notadd/ng-material-pro': 'Angular material2 Extension Components ..', - '@notadd/ng-material2': 'Angular material2 Extension Components ..', - '@nrwl/angular': 'Angular Plugin for Nx', - '@nrwl/cypress': 'Cypress plugin for Nx', - '@nrwl/express': 'Express Plugin for Nx', - '@nrwl/jest': 'Jest plugin for Nx', - '@nrwl/nest': 'Nest Plugin for Nx', - '@nrwl/node': 'Node Plugin for Nx', - '@nrwl/react': 'React Plugin for Nx', - '@nrwl/schematics': - 'Angular CLI power-ups for modern Web development: Schematics', - '@nrwl/web': 'Web Plugin for Nx', - '@nrwl/workspace': 'Power-ups for Angular CLI', - '@nstudio/schematics': 'Cross-platform (xplat) tools for Nx workspaces.', - '@ockilson/local-schematics': - 'Setup project specific schematics without bundling npm packages', - '@ockilson/ng-jest': 'Schematic to setup jest for angular/cli projects', - '@ockilson/ng-storybook': - 'Setup project specific schematics without bundling npm packages', - '@ockilson/schematics': - 'Schematic to run through default setup of angular app (very opinionated)', - '@oktadev/schematics': 'Schematics for Okta Auth', - '@pascaliske/schematics': - 'Angular schematics collection for integrating setup tools like prettier and storybook.', - '@paultaku/angular-schematics': 'A blank schematics', - '@progress/kendo-angular-conversational-ui': - 'Kendo UI for Angular Conversational UI components', - '@progress/kendo-angular-excel-export': - 'Kendo UI for Angular Excel Export component', - '@progress/kendo-angular-menu': 'Kendo UI Angular Menu component', - '@progress/kendo-angular-pdf-export': - 'Kendo UI for Angular PDF Export Component', - '@progress/kendo-angular-popup': 'Kendo UI Angular 2 Popup component', - '@progress/kendo-angular-scrollview': 'A ScrollView Component for Angular 2', - '@progress/kendo-angular-toolbar': - 'Kendo UI Angular 2 component starter template', - '@progress/kendo-angular-upload': 'Kendo UI Angular 2 Upload Component', - '@progress/kendo-schematics': 'Kendo UI Schematics for Angular', - '@quramy/angular-lang-service': - 'A schematics to add @angular/language-service', - '@schuchard/prettier': 'An Angular schematic for adding prettier', - '@slupekdev/vscode': - 'Add recommended extensions and configuration to an Angular CLI project', - '@supine/sofa': 'Angular sofa', - '@syncfusion/ej2-angular-base': - 'A common package of Essential JS 2 base Angular libraries, methods and class definitions', - '@syncfusion/ej2-angular-buttons': - 'A package of feature-rich Essential JS 2 components such as Button, CheckBox, RadioButton and Switch. for Angular', - '@syncfusion/ej2-angular-calendars': - 'A complete package of date or time components with built-in features such as date formatting, inline editing, multiple (range) selection, range restriction, month and year selection, strict mode, and globalization. for Angular', - '@syncfusion/ej2-angular-charts': - 'Feature-rich chart control with built-in support for over 25 chart types, technical indictors, trendline, zooming, tooltip, selection, crosshair and trackball. for Angular', - '@syncfusion/ej2-angular-circulargauge': - 'Essential JS 2 CircularGauge Components for Angular', - '@syncfusion/ej2-angular-diagrams': - 'Feature-rich diagram control to create diagrams like flow charts, organizational charts, mind maps, and BPMN diagrams. Its rich feature set includes built-in shapes, editing, serializing, exporting, printing, overview, data binding, and automatic layouts.', - '@syncfusion/ej2-angular-documenteditor': - 'Feature-rich document editor control with built-in support for context menu, options pane and dialogs. for Angular', - '@syncfusion/ej2-angular-dropdowns': - 'Essential JS 2 DropDown Components for Angular', - '@syncfusion/ej2-angular-filemanager': - 'Essential JS 2 FileManager Component for Angular', - '@syncfusion/ej2-angular-gantt': 'Essential JS 2 Gantt Component for Angular', - '@syncfusion/ej2-angular-grids': - 'Feature-rich JavaScript datagrid (datatable) control with built-in support for editing, filtering, grouping, paging, sorting, and exporting to Excel. for Angular', - '@syncfusion/ej2-angular-heatmap': - 'Feature rich data visulization control used to visualize the matrix data where the individual values are represented as colors for Angular', - '@syncfusion/ej2-angular-inplace-editor': - 'A package of Essential JS 2 Inplace editor components, which is used to edit and update the value dynamically in server. for Angular', - '@syncfusion/ej2-angular-inputs': - 'A package of Essential JS 2 input components such as Textbox, Color-picker, Masked-textbox, Numeric-textbox, Slider, Upload, and Form-validator that is used to get input from the users. for Angular', - '@syncfusion/ej2-angular-layouts': - 'A package of Essential JS 2 layout pure CSS components such as card and avatar. The card is used as small container to show content in specific structure, whereas the avatars are icons, initials or figures representing particular person. for Angular', - '@syncfusion/ej2-angular-lineargauge': - 'Essential JS 2 LinearGauge Components for Angular', - '@syncfusion/ej2-angular-lists': - 'The listview control allows you to select an item or multiple items from a list-like interface and represents the data in interactive hierarchical structure across different layouts or views. for Angular', - '@syncfusion/ej2-angular-maps': - 'The Maps component is used to visualize the geographical data and represent the statistical data of a particular geographical area on earth with user interactivity, and provides various customizing options for Angular', - '@syncfusion/ej2-angular-navigations': - 'A package of Essential JS 2 navigation components such as Tree-view, Tab, Toolbar, Context-menu, and Accordion which is used to navigate from one page to another for Angular', - '@syncfusion/ej2-angular-notifications': - 'A package of Essential JS 2 notification components such as Toast and Badge which used to notify important information to end-users. for Angular', - '@syncfusion/ej2-angular-pdfviewer': - 'Essential JS 2 PDF viewer Component for Angular', - '@syncfusion/ej2-angular-pivotview': - 'The pivot grid, or pivot table, is used to visualize large sets of relational data in a cross-tabular format, similar to an Excel pivot table. for Angular', - '@syncfusion/ej2-angular-popups': - 'A package of Essential JS 2 popup components such as Dialog and Tooltip that is used to display information or messages in separate pop-ups. for Angular', - '@syncfusion/ej2-angular-querybuilder': - 'Essential JS 2 QueryBuilder for Angular', - '@syncfusion/ej2-angular-richtexteditor': - 'Essential JS 2 RichTextEditor component for Angular', - '@syncfusion/ej2-angular-schedule': - 'Flexible scheduling library with more built-in features and enhanced customization options similar to outlook and google calendar, allowing the users to plan and manage their appointments with efficient data-binding support. for Angular', - '@syncfusion/ej2-angular-splitbuttons': - 'A package of feature-rich Essential JS 2 components such as DropDownButton, SplitButton, ProgressButton and ButtonGroup. for Angular', - '@syncfusion/ej2-angular-treegrid': - 'Essential JS 2 TreeGrid Component for Angular', - '@syncfusion/ej2-angular-treemap': - 'Essential JS 2 TreeMap Components for Angular', - '@syncfusion/ej2-ng-base': - 'A common package of Essential JS 2 base Angular libraries, methods and class definitions', - '@vendasta/material': 'Angular Material', - '@willh/hmr': - 'Enabling Hot Module Replacement (HMR) feature in your Angular CLI v6 project', - 'angular-cli-ghpages': - 'Deploy your Angular app to GitHub pages directly from the Angular CLI', - 'angular-fire-schematics': 'AngularFire Schematics', - 'angular-karma-gwt': - 'Schematics to update the default karma config file created by the Angluar Cli to integrate jasmine-given and mocha-reporter.', - 'angular-made-with-love': - '🚀 An experimental project which demonstrates an Angular Package which contains Angular Elements and Schematics', - 'angular-playground': - 'A drop in app module for working on Angular components in isolation (aka Scenario Driven Development).', - 'angular-popper': - 'Popover component for Angular 2+ based on Popper.js library.', - 'angular-vscode': 'Useful VSCode plugins for Angular Development', - 'ant-reset-private': - 'An enterprise-class UI components based on Ant Design and Angular', - 'apollo-angular': - 'Use your GraphQL data in your Angular app, with the Apollo Client', - 'at-ng': '', - 'bootstrap-schematics': 'Bootstrap Options Schema', - 'devextreme-schematics': - 'DevExtreme schematics are tools that you can use to add DevExtreme sources, views, and layout templates to Angular applications.', - 'guozhiqing-momentum': - 'Momentum is an Angular Schematic developed by the Bottle Rocket Web Team to build best-in-class web applications faster.', - 'hmr-enabled': - 'Enabling Hot Module Replacement (HMR) feature in your Angular CLI v6 project', - 'host-antd': - 'An enterprise-class UI components based on Ant Design and Angular', - 'ican-ng-zorro-antd': - 'An enterprise-class UI components based on Ant Design and Angular', - 'igniteui-angular': - 'Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps', - 'json-server-schematics': - 'Angular schematics for adding json-server to an Angular workspace', - narik: 'Framework to create angular application', - 'nebular-schematics-test-theme': 'nebular-schematics-test-theme', - 'ng-cli-pug-loader': - 'An schematic to add support for .pug files on Angular projects', - 'ng-cosmos-ui': - 'An enterprise-class UI components based on Ant Design and Angular', - 'ng-danielszenasi-antd': - 'An enterprise-class UI components based on Ant Design and Angular', - 'ng-dockerize': 'Schematic that adds configuration to run angular in docker', - 'ng-lists': 'List Components for Angular', - 'ng-momentum': - 'Momentum is an Angular Schematic developed by the Bottle Rocket Web Team to build best-in-class web applications faster.', - 'ng-universal-k8s': - 'Angular schematic to add Kubernetes functionality to Angular Universal', - 'ng-z-atsale': - 'An enterprise-class UI components based on Ant Design and Angular', - 'ng-zorro-antd': - 'An enterprise-class UI components based on Ant Design and Angular', - 'ng-zorro-antd-mobile': - 'An enterprise-class mobile UI components based on Ant Design and Angular', - 'ng-zorro-antd-net': - 'An enterprise-class UI components based on Ant Design and Angular', - 'ng-zorro-antd-wendzhue-fake': - 'An enterprise-class UI components based on Ant Design and Angular', - 'ng-zorro-antd-xinhai': - 'An enterprise-class UI components based on Ant Design and Angular', - 'ng-zorro-antd-yj': - 'An enterprise-class UI components based on Ant Design and Angular', - 'ngcli-wallaby': 'A schematic to add wallabyJS config to Angular project', - 'ngx-agora': - 'Angular 7 wrapper for Agora RTC client (https://www.agora.io/en/)', - 'ngx-animated-gradient': - 'Angular Directive that animated the gradient background', - 'ngx-auth-firebaseui': - 'Open Source Library for Angular Web Apps to integrate a material user interface for firebase authentication', - 'ngx-bootstrap': 'Native Angular Bootstrap Components', - 'ngx-bootstrap-ci': 'Native Angular Bootstrap Components', - 'ngx-bootstrap-th': 'Native Angular Bootstrap Components', - 'ngx-build-modern': - 'Turnkey solution for differential serving in Angular. Serve fewer bytes -\u003e increase performance', - 'ngx-build-plus': "Extends the Angular CLI's build process!", - 'ngx-face-api-js': - 'Angular directives for face detection and face recognition in the browser. It is a wrapper for face-api.js, so it is not dependent on the browser implementation.', - 'ngx-onesignal': 'angular 7+ OneSignal Service', - 'ngx-semantic-version': - 'Add and configure commitlint, commitizen, husky and standard-version for creating conventional commits and automate your release and CHANGELOG generation respecting semver', - 'ngx-weui': 'WeUI for angular', - 'primeng-schematics': 'Schematics for Prime NG', - 'puppeteer-schematic': - 'An Angular Schematic to add Chrome Headless instead of PhantomJS', - 'pz-nz-component': - 'An enterprise-class UI components based on Ant Design and Angular', - 'rocky-schematics': 'rocky-schematics collections for angular-cli', - 'shallow-render-schematics': 'Shallow rendering test utility for Angular', - 'testowa-libka': - '🚀 An experimental project which demonstrates an Angular Package which contains Angular Elements and Schematics', - 'typewiz-angular': - 'An Angular Schematic that automatically adds types to TypeScript code using [TypeWiz](https://www.npmjs.com/package/typewiz-core)', - 'ui-jar-schematics': - 'Schematics that add an ui-jar project in an Angular workspace for making documentation', - 'yang-schematics': 'Yet Another Angular Generator', -}; - -export function readExtensions(packageJson: any) { - return availableExtensions().filter((e) => { - const hasDep = packageJson.dependencies && packageJson.dependencies[e.name]; - const hasDevDep = - packageJson.devDependencies && packageJson.devDependencies[e.name]; - return hasDep || hasDevDep; - }); -} - -export function availableExtensions(): Array<{ - name: string; - description: string; -}> { - return Object.keys(EXTENSIONS) - .sort() - .map((name) => { - const description = EXTENSIONS[name]; - return { - name, - description, - }; - }); -} diff --git a/libs/server/src/lib/select-generator.ts b/libs/server/src/lib/select-generator.ts deleted file mode 100644 index f70c522991..0000000000 --- a/libs/server/src/lib/select-generator.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { CollectionInfo, Generator, GeneratorType } from '@nx-console/schema'; -import { TaskExecutionSchema } from '@nx-console/schema'; -import { QuickPickItem, window } from 'vscode'; -import { getGenerators, readGeneratorOptions } from './utils/get-generators'; - -export async function selectGenerator( - workspaceJsonPath: string, - workspaceType: 'nx' | 'ng', - generatorType?: GeneratorType -): Promise { - interface GenerateQuickPickItem extends QuickPickItem { - collectionName: string; - generator: Generator; - } - - const generators = await getGenerators(workspaceJsonPath, workspaceType); - let generatorsQuickPicks = generators - .filter((c) => !!c.data) - .map((c): GenerateQuickPickItem => { - const generatorData = c.data!; - return { - description: generatorData.description, - label: `${generatorData.collection} - ${generatorData.name}`, - collectionName: generatorData.collection, - generator: generatorData, - }; - }); - - if (generatorType) { - generatorsQuickPicks = generatorsQuickPicks.filter((generator) => { - return generator.generator.type === generatorType; - }); - } - - if (generators) { - const selection = await window.showQuickPick(generatorsQuickPicks); - if (selection) { - const options = - selection.generator.options || - (await readGeneratorOptions( - workspaceJsonPath, - selection.collectionName, - selection.generator.name - )); - const positional = `${selection.collectionName}:${selection.generator.name}`; - return { - ...selection.generator, - options, - command: 'generate', - positional, - cliName: 'nx', - }; - } - } -} diff --git a/libs/server/src/lib/utils/get-generators.ts b/libs/server/src/lib/utils/get-generators.ts index b621b1dc37..be69b659a7 100644 --- a/libs/server/src/lib/utils/get-generators.ts +++ b/libs/server/src/lib/utils/get-generators.ts @@ -1,10 +1,5 @@ -import { - Option, - Generator, - CollectionInfo, - GeneratorType, -} from '@nx-console/schema'; -import { basename, dirname, join } from 'path'; +import { CollectionInfo, GeneratorType } from '@nx-console/schema'; +import { basename, join } from 'path'; import { directoryExists, @@ -12,7 +7,6 @@ import { listFiles, normalizeSchema, readAndCacheJsonFile, - toWorkspaceFormat, } from './utils'; import { getCollectionInfo, @@ -34,12 +28,12 @@ export async function getGenerators( generatorCollections = [ ...generatorCollections, - ...(await checkAndReadWorkspaceCollection( + ...(await checkAndReadWorkspaceGenerators( basedir, join('tools', 'schematics'), workspaceType )), - ...(await checkAndReadWorkspaceCollection( + ...(await checkAndReadWorkspaceGenerators( basedir, join('tools', 'generators'), workspaceType @@ -50,7 +44,7 @@ export async function getGenerators( ); } -async function checkAndReadWorkspaceCollection( +async function checkAndReadWorkspaceGenerators( basedir: string, workspaceGeneratorsPath: string, workspaceType: 'nx' | 'ng' @@ -66,36 +60,6 @@ async function checkAndReadWorkspaceCollection( return Promise.resolve([]); } -async function readWorkspaceJsonDefaults( - workspaceJsonPath: string -): Promise { - const workspaceJson = await readAndCacheJsonFile(workspaceJsonPath); - const defaults = toWorkspaceFormat(workspaceJson.json).generators || {}; - const collectionDefaults = Object.keys(defaults).reduce( - (collectionDefaultsMap: any, key) => { - if (key.includes(':')) { - const [collectionName, generatorName] = key.split(':'); - if (!collectionDefaultsMap[collectionName]) { - collectionDefaultsMap[collectionName] = {}; - } - collectionDefaultsMap[collectionName][generatorName] = defaults[key]; - } else { - const collectionName = key; - if (!collectionDefaultsMap[collectionName]) { - collectionDefaultsMap[collectionName] = {}; - } - Object.keys(defaults[collectionName]).forEach((generatorName) => { - collectionDefaultsMap[collectionName][generatorName] = - defaults[collectionName][generatorName]; - }); - } - return collectionDefaultsMap; - }, - {} - ); - return collectionDefaults; -} - async function readWorkspaceGeneratorsCollection( basedir: string, workspaceGeneratorsPath: string, @@ -114,6 +78,7 @@ async function readWorkspaceGeneratorsCollection( return getCollectionInfo( collectionName, collectionPath, + collectionDir, {}, collection.json ); @@ -123,12 +88,13 @@ async function readWorkspaceGeneratorsCollection( .filter((f) => basename(f) === 'schema.json') .map(async (f) => { const schemaJson = await readAndCacheJsonFile(f, ''); + const name = schemaJson.json.id || schemaJson.json.$id; return { name: collectionName, type: 'generator', path: collectionDir, data: { - name: schemaJson.json.id || schemaJson.json.$id, + name, collection: collectionName, options: await normalizeSchema(schemaJson.json), description: '', @@ -139,37 +105,3 @@ async function readWorkspaceGeneratorsCollection( ); } } - -export async function readGeneratorOptions( - workspaceJsonPath: string, - collectionName: string, - generatorName: string -): Promise { - const basedir = join(workspaceJsonPath, '..'); - const nodeModulesDir = join(basedir, 'node_modules'); - const collectionPackageJson = await readAndCacheJsonFile( - join(collectionName, 'package.json'), - nodeModulesDir - ); - const collectionJson = await readAndCacheJsonFile( - collectionPackageJson.json.schematics || - collectionPackageJson.json.generators, - dirname(collectionPackageJson.path) - ); - const generators = Object.assign( - {}, - collectionJson.json.schematics, - collectionJson.json.generators - ); - - const generatorSchema = await readAndCacheJsonFile( - generators[generatorName].schema, - dirname(collectionJson.path) - ); - const workspaceDefaults = await readWorkspaceJsonDefaults(workspaceJsonPath); - const defaults = - workspaceDefaults && - workspaceDefaults[collectionName] && - workspaceDefaults[collectionName][generatorName]; - return await normalizeSchema(generatorSchema.json, defaults); -} diff --git a/libs/server/src/lib/utils/read-collections.ts b/libs/server/src/lib/utils/read-collections.ts index 99816c55ee..4229da6ee3 100644 --- a/libs/server/src/lib/utils/read-collections.ts +++ b/libs/server/src/lib/utils/read-collections.ts @@ -34,21 +34,37 @@ export async function readCollectionsFromNodeModules( }) ); - const collectionMap = await Promise.all( - collections.map((c) => readCollections(nodeModulesDir, c.packageName)) - ); + const allCollections = ( + await Promise.all( + collections.map((c) => readCollections(nodeModulesDir, c.packageName)) + ) + ).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(); + for (const singleCollection of allCollections) { + if (!singleCollection) { + continue; + } - return collectionMap.flat().filter((c): c is CollectionInfo => Boolean(c)); + if (!dedupedCollections.has(singleCollection.name)) { + dedupedCollections.set(singleCollection.name, singleCollection); + } + } + + return Array.from(dedupedCollections.values()); } export async function readCollections( - basedir: string, + nodeModulesDir: string, collectionName: string ): Promise { try { const packageJson = await readAndCacheJsonFile( join(collectionName, 'package.json'), - basedir + nodeModulesDir ); const [executorCollections, generatorCollections] = await Promise.all([ @@ -65,6 +81,7 @@ export async function readCollections( return getCollectionInfo( collectionName, packageJson.path, + nodeModulesDir, executorCollections.json, generatorCollections.json ); @@ -73,15 +90,16 @@ export async function readCollections( } } -export function getCollectionInfo( +export async function getCollectionInfo( collectionName: string, path: string, + collectionDir: string, executorCollectionJson: any, generatorCollectionJson: any -): CollectionInfo[] { +): Promise { const baseDir = dirname(path); - const collection: CollectionInfo[] = []; + const collectionMap: Map = new Map(); const buildCollectionInfo = ( name: string, @@ -105,11 +123,14 @@ export function getCollectionInfo( for (const [key, schema] of Object.entries( executorCollectionJson.executors || executorCollectionJson.executors || {} )) { - if (!canUse(collectionName, schema)) { + if (!canUse(key, schema)) { continue; } - - collection.push(buildCollectionInfo(key, schema, 'executor')); + const collectionInfo = buildCollectionInfo(key, schema, 'executor'); + if (collectionMap.has(collectionInfo.name)) { + continue; + } + collectionMap.set(collectionInfo.name, collectionInfo); } for (const [key, schema] of Object.entries( @@ -117,7 +138,7 @@ export function getCollectionInfo( generatorCollectionJson.schematics || {} )) { - if (!canUse(collectionName, schema)) { + if (!canUse(key, schema)) { continue; } @@ -128,13 +149,40 @@ export function getCollectionInfo( key, schema ); - collection.push(collectionInfo); + if (collectionMap.has(collectionInfo.name)) { + continue; + } + collectionMap.set(collectionInfo.name, collectionInfo); } catch (e) { // noop - generator is invalid } } - return collection; + if ( + generatorCollectionJson.extends && + Array.isArray(generatorCollectionJson.extends) + ) { + const extendedSchema = generatorCollectionJson.extends as string[]; + const extendedCollections = ( + await Promise.all( + extendedSchema + .filter((extended) => extended !== '@nrwl/workspace') + .map((extended: string) => readCollections(collectionDir, extended)) + ) + ) + .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( diff --git a/libs/vscode/tasks/src/index.ts b/libs/vscode/tasks/src/index.ts index bd0b104d09..89f8f03d28 100644 --- a/libs/vscode/tasks/src/index.ts +++ b/libs/vscode/tasks/src/index.ts @@ -2,3 +2,4 @@ export * from './lib/cli-task-provider'; export * from './lib/nx-task-commands'; export * from './lib/cli-task-commands'; export * from './lib/cli-task-quick-pick-item'; +export * from './lib/select-generator'; diff --git a/libs/vscode/tasks/src/lib/cli-task-commands.ts b/libs/vscode/tasks/src/lib/cli-task-commands.ts index ddcbee985f..1b8f400821 100644 --- a/libs/vscode/tasks/src/lib/cli-task-commands.ts +++ b/libs/vscode/tasks/src/lib/cli-task-commands.ts @@ -1,6 +1,5 @@ import { commands, ExtensionContext, window, Uri } from 'vscode'; -import { selectGenerator } from '@nx-console/server'; import { verifyWorkspace } from '@nx-console/vscode/nx-workspace'; import { verifyBuilderDefinition } from '@nx-console/vscode/verify'; import { @@ -13,6 +12,7 @@ import { selectFlags } from './select-flags'; import { GeneratorType, Option } from '@nx-console/schema'; import { OptionType } from '@angular/cli/models/interface'; import { WorkspaceJsonConfiguration } from '@nrwl/devkit'; +import { selectGenerator } from './select-generator'; const CLI_COMMAND_LIST = [ 'build', diff --git a/libs/vscode/tasks/src/lib/select-generator.ts b/libs/vscode/tasks/src/lib/select-generator.ts new file mode 100644 index 0000000000..2623cbeebf --- /dev/null +++ b/libs/vscode/tasks/src/lib/select-generator.ts @@ -0,0 +1,146 @@ +import { + CollectionInfo, + Generator, + GeneratorType, + Option, + TaskExecutionSchema, +} from '@nx-console/schema'; +import { QuickPickItem, window } from 'vscode'; +import { + getGenerators, + normalizeSchema, + readAndCacheJsonFile, +} from '@nx-console/server'; +import { getNxConfig } from '@nx-console/vscode/nx-workspace'; +import { dirname, join } from 'path'; + +async function readWorkspaceJsonDefaults( + workspaceJsonPath: string +): Promise { + const workspaceJson = await readAndCacheJsonFile(workspaceJsonPath); + let defaults = workspaceJson.json.schematics || workspaceJson.json.generators; + + if (!defaults) { + try { + /** + * This could potentially fail if we're in an Angular CLI project without schematics being part of angular.json + * Default the default to {} on the catch + */ + defaults = + (await getNxConfig(dirname(workspaceJsonPath))).generators || {}; + } catch (e) { + defaults = {}; + } + } + + const collectionDefaults = Object.keys(defaults).reduce( + (collectionDefaultsMap: any, key) => { + if (key.includes(':')) { + const [collectionName, generatorName] = key.split(':'); + if (!collectionDefaultsMap[collectionName]) { + collectionDefaultsMap[collectionName] = {}; + } + collectionDefaultsMap[collectionName][generatorName] = defaults[key]; + } else { + const collectionName = key; + if (!collectionDefaultsMap[collectionName]) { + collectionDefaultsMap[collectionName] = {}; + } + Object.keys(defaults[collectionName]).forEach((generatorName) => { + collectionDefaultsMap[collectionName][generatorName] = + defaults[collectionName][generatorName]; + }); + } + return collectionDefaultsMap; + }, + {} + ); + return collectionDefaults; +} + +export async function readGeneratorOptions( + workspaceJsonPath: string, + collectionName: string, + generatorName: string +): Promise { + const basedir = join(workspaceJsonPath, '..'); + const nodeModulesDir = join(basedir, 'node_modules'); + const collectionPackageJson = await readAndCacheJsonFile( + join(collectionName, 'package.json'), + nodeModulesDir + ); + const collectionJson = await readAndCacheJsonFile( + collectionPackageJson.json.schematics || + collectionPackageJson.json.generators, + dirname(collectionPackageJson.path) + ); + const generators = Object.assign( + {}, + collectionJson.json.schematics, + collectionJson.json.generators + ); + + const generatorSchema = await readAndCacheJsonFile( + generators[generatorName].schema, + dirname(collectionJson.path) + ); + const workspaceDefaults = await readWorkspaceJsonDefaults(workspaceJsonPath); + const defaults = + workspaceDefaults && + workspaceDefaults[collectionName] && + workspaceDefaults[collectionName][generatorName]; + return await normalizeSchema(generatorSchema.json, defaults); +} + +export async function selectGenerator( + workspaceJsonPath: string, + workspaceType: 'nx' | 'ng', + generatorType?: GeneratorType +): Promise { + interface GenerateQuickPickItem extends QuickPickItem { + collectionName: string; + generator: Generator; + } + + const generators = await getGenerators(workspaceJsonPath, workspaceType); + let generatorsQuickPicks = generators + .map((c) => c.data) + .filter( + (generator: Generator | undefined): generator is Generator => !!generator + ) + .map((generatorData): GenerateQuickPickItem => { + return { + description: generatorData.description, + label: `${generatorData.collection} - ${generatorData.name}`, + collectionName: generatorData.collection, + generator: generatorData, + }; + }); + + if (generatorType) { + generatorsQuickPicks = generatorsQuickPicks.filter((generator) => { + return generator.generator.type === generatorType; + }); + } + + if (generators) { + const selection = await window.showQuickPick(generatorsQuickPicks); + if (selection) { + const options = + selection.generator.options || + (await readGeneratorOptions( + workspaceJsonPath, + selection.collectionName, + selection.generator.name + )); + const positional = `${selection.collectionName}:${selection.generator.name}`; + return { + ...selection.generator, + options, + command: 'generate', + positional, + cliName: 'nx', + }; + } + } +} diff --git a/libs/vscode/webview/src/lib/get-task-execution-schema.ts b/libs/vscode/webview/src/lib/get-task-execution-schema.ts index 82e224c648..c39602d36c 100644 --- a/libs/vscode/webview/src/lib/get-task-execution-schema.ts +++ b/libs/vscode/webview/src/lib/get-task-execution-schema.ts @@ -3,7 +3,6 @@ import { getOutputChannel, getTelemetry, readTargetDef, - selectGenerator, } from '@nx-console/server'; import { getNxConfig, verifyWorkspace } from '@nx-console/vscode/nx-workspace'; import { verifyBuilderDefinition } from '@nx-console/vscode/verify'; @@ -13,6 +12,7 @@ import { CliTaskProvider, CliTaskQuickPickItem, selectCliProject, + selectGenerator, } from '@nx-console/vscode/tasks'; export async function getTaskExecutionSchema( From 00c5be0d42e623df80dfe827679a13404095c998 Mon Sep 17 00:00:00 2001 From: Jonathan Cammisuli Date: Fri, 22 Oct 2021 15:16:40 -0400 Subject: [PATCH 4/7] clean up --- apps/vscode/src/main.ts | 2 +- libs/server/src/lib/utils/get-generators.ts | 25 ++++++++----------- libs/vscode/tasks/src/lib/select-generator.ts | 2 +- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index b9cd1b78b6..71e4f4ffc7 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -291,7 +291,7 @@ async function setApplicationAndLibraryContext(workspaceJsonPath: string) { ), ]); - const generatorCollections = await getGenerators(workspaceJsonPath, 'nx'); + const generatorCollections = await getGenerators(workspaceJsonPath); let hasApplicationGenerators = false; let hasLibraryGenerators = false; diff --git a/libs/server/src/lib/utils/get-generators.ts b/libs/server/src/lib/utils/get-generators.ts index be69b659a7..6522c41155 100644 --- a/libs/server/src/lib/utils/get-generators.ts +++ b/libs/server/src/lib/utils/get-generators.ts @@ -14,8 +14,7 @@ import { } from './read-collections'; export async function getGenerators( - workspaceJsonPath: string, - workspaceType: 'nx' | 'ng' + workspaceJsonPath: string ): Promise { const basedir = join(workspaceJsonPath, '..'); const collections = await readCollectionsFromNodeModules( @@ -30,13 +29,11 @@ export async function getGenerators( ...generatorCollections, ...(await checkAndReadWorkspaceGenerators( basedir, - join('tools', 'schematics'), - workspaceType + join('tools', 'schematics') )), ...(await checkAndReadWorkspaceGenerators( basedir, - join('tools', 'generators'), - workspaceType + join('tools', 'generators') )), ]; return generatorCollections.filter( @@ -46,14 +43,12 @@ export async function getGenerators( async function checkAndReadWorkspaceGenerators( basedir: string, - workspaceGeneratorsPath: string, - workspaceType: 'nx' | 'ng' + workspaceGeneratorsPath: string ) { if (await directoryExists(join(basedir, workspaceGeneratorsPath))) { const collection = await readWorkspaceGeneratorsCollection( basedir, - workspaceGeneratorsPath, - workspaceType + workspaceGeneratorsPath ); return collection; } @@ -62,12 +57,10 @@ async function checkAndReadWorkspaceGenerators( async function readWorkspaceGeneratorsCollection( basedir: string, - workspaceGeneratorsPath: string, - workspaceType: 'nx' | 'ng' + workspaceGeneratorsPath: string ): Promise { const collectionDir = join(basedir, workspaceGeneratorsPath); - const collectionName = - workspaceType === 'nx' ? 'workspace-generator' : 'workspace-schematic'; + const collectionName = 'workspace-generator'; const collectionPath = join(collectionDir, 'collection.json'); if (fileExistsSync(collectionPath)) { const collection = await readAndCacheJsonFile( @@ -89,6 +82,8 @@ async function readWorkspaceGeneratorsCollection( .map(async (f) => { const schemaJson = await readAndCacheJsonFile(f, ''); const name = schemaJson.json.id || schemaJson.json.$id; + const type: GeneratorType = + schemaJson.json['x-type'] ?? GeneratorType.Other; return { name: collectionName, type: 'generator', @@ -98,7 +93,7 @@ async function readWorkspaceGeneratorsCollection( collection: collectionName, options: await normalizeSchema(schemaJson.json), description: '', - type: GeneratorType.Other, + type, }, } as CollectionInfo; }) diff --git a/libs/vscode/tasks/src/lib/select-generator.ts b/libs/vscode/tasks/src/lib/select-generator.ts index 2623cbeebf..eaa512ed8e 100644 --- a/libs/vscode/tasks/src/lib/select-generator.ts +++ b/libs/vscode/tasks/src/lib/select-generator.ts @@ -102,7 +102,7 @@ export async function selectGenerator( generator: Generator; } - const generators = await getGenerators(workspaceJsonPath, workspaceType); + const generators = await getGenerators(workspaceJsonPath); let generatorsQuickPicks = generators .map((c) => c.data) .filter( From 40bca0588d9b224e0eeef5896128550ed3e05f1f Mon Sep 17 00:00:00 2001 From: Jonathan Cammisuli Date: Fri, 22 Oct 2021 15:45:25 -0400 Subject: [PATCH 5/7] handle nxjson not existing --- apps/vscode/src/main.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index 71e4f4ffc7..2af14deb49 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -276,7 +276,12 @@ async function setWorkspace(workspaceJsonPath: string) { async function setApplicationAndLibraryContext(workspaceJsonPath: string) { const { getNxConfig } = await import('@nx-console/vscode/nx-workspace'); - const nxConfig = await getNxConfig(dirname(workspaceJsonPath)); + let nxConfig: Awaited>; + try { + nxConfig = await getNxConfig(dirname(workspaceJsonPath)); + } catch { + return; + } commands.executeCommand('setContext', 'nxAppsDir', [ join( From 220b7e2f04d9cf33e138d8ab1a36f1f12d180719 Mon Sep 17 00:00:00 2001 From: Jonathan Cammisuli Date: Fri, 22 Oct 2021 15:58:39 -0400 Subject: [PATCH 6/7] provide types for awaited --- apps/vscode/src/main.ts | 2 ++ libs/schema/src/index.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index 2af14deb49..d4cacad66a 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -45,6 +45,7 @@ import { NxProjectTreeProvider, } from '@nx-console/vscode/nx-project-view'; import { environment } from './environments/environment'; +import { Awaited } from '@nx-console/schema'; import { WorkspaceJsonSchema, @@ -276,6 +277,7 @@ async function setWorkspace(workspaceJsonPath: string) { async function setApplicationAndLibraryContext(workspaceJsonPath: string) { const { getNxConfig } = await import('@nx-console/vscode/nx-workspace'); + let nxConfig: Awaited>; try { nxConfig = await getNxConfig(dirname(workspaceJsonPath)); diff --git a/libs/schema/src/index.ts b/libs/schema/src/index.ts index 9929750817..55fe774201 100644 --- a/libs/schema/src/index.ts +++ b/libs/schema/src/index.ts @@ -96,3 +96,15 @@ export interface Targets { export const WORKSPACE_GENERATOR_NAME_REGEX = /^workspace-(schematic|generator):(.+)/; + +/** + * Should be in Typescript 4.4+ remove this when we upgrade to that version + */ +export type Awaited = T extends null | undefined + ? T // special case for `null | undefined` when not in `--strictNullChecks` mode + : // eslint-disable-next-line @typescript-eslint/ban-types + T extends object & { then(onfulfilled: infer F): any } // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped + ? F extends (value: infer V) => any // if the argument to `then` is callable, extracts the argument + ? Awaited // recursively unwrap the value + : never // the argument to `then` was not callable + : T; From 42c5e57a99d53d16a8d5e13ae2d5789318e73e0a Mon Sep 17 00:00:00 2001 From: Jonathan Cammisuli Date: Fri, 22 Oct 2021 16:32:11 -0400 Subject: [PATCH 7/7] readd listOfUnnestedNpmPackages for angular cli support --- libs/server/src/lib/utils/read-collections.ts | 15 +++---- libs/server/src/lib/utils/utils.ts | 45 +++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/libs/server/src/lib/utils/read-collections.ts b/libs/server/src/lib/utils/read-collections.ts index 4229da6ee3..6c8ad580b9 100644 --- a/libs/server/src/lib/utils/read-collections.ts +++ b/libs/server/src/lib/utils/read-collections.ts @@ -1,7 +1,11 @@ import { platform } from 'os'; import { dirname, join } from 'path'; import { CollectionInfo, Generator, GeneratorType } from '@nx-console/schema'; -import { clearJsonCache, readAndCacheJsonFile } from './utils'; +import { + clearJsonCache, + listOfUnnestedNpmPackages, + readAndCacheJsonFile, +} from './utils'; export async function readCollectionsFromNodeModules( workspaceJsonPath: string, @@ -14,15 +18,10 @@ export async function readCollectionsFromNodeModules( clearJsonCache('package.json', basedir); } - const packageJson = (await readAndCacheJsonFile('package.json', basedir)) - .json; - const packages: { [packageName: string]: string } = { - ...(packageJson.devDependencies || {}), - ...(packageJson.dependencies || {}), - }; + const packages = await listOfUnnestedNpmPackages(nodeModulesDir); const collections = await Promise.all( - Object.keys(packages).map(async (p) => { + packages.map(async (p) => { const json = await readAndCacheJsonFile( join(p, 'package.json'), nodeModulesDir diff --git a/libs/server/src/lib/utils/utils.ts b/libs/server/src/lib/utils/utils.ts index 3d3e3f3f8b..573a9e4c45 100644 --- a/libs/server/src/lib/utils/utils.ts +++ b/libs/server/src/lib/utils/utils.ts @@ -18,6 +18,7 @@ import { ParseError, printParseErrorCode, } from 'jsonc-parser'; +import { readdir } from 'fs/promises'; import * as path from 'path'; import { getOutputChannel } from './output-channel'; @@ -39,6 +40,50 @@ const IMPORTANT_FIELD_NAMES = [ ]; const IMPORTANT_FIELDS_SET = new Set(IMPORTANT_FIELD_NAMES); +/** + * Get a flat list of all node_modules folders in the workspace. + * This is needed to continue to support Angular CLI projects. + * + * @param nodeModulesDir + * @returns + */ +export async function listOfUnnestedNpmPackages( + nodeModulesDir: string +): Promise { + const res: string[] = []; + const stats = await stat(nodeModulesDir); + if (!stats.isDirectory()) { + return res; + } + + const dirContents = await readdir(nodeModulesDir); + + for (const npmPackageOrScope of dirContents) { + if (npmPackageOrScope.startsWith('.')) { + continue; + } + + const packageStats = await stat( + path.join(nodeModulesDir, npmPackageOrScope) + ); + if (!packageStats.isDirectory()) { + continue; + } + + if (npmPackageOrScope.startsWith('@')) { + (await readdir(path.join(nodeModulesDir, npmPackageOrScope))).forEach( + (p) => { + res.push(`${npmPackageOrScope}/${p}`); + } + ); + } else { + res.push(npmPackageOrScope); + } + } + + return res; +} + export function listFiles(dirName: string): string[] { // TODO use .gitignore to skip files if (dirName.indexOf('node_modules') > -1) return [];