From d63ee1560f3c312d554c3220ace57fb8a15f06e3 Mon Sep 17 00:00:00 2001 From: Emily Xiong Date: Thu, 30 Mar 2023 22:44:07 -0400 Subject: [PATCH] feat(core): update create-nx-plugin to generate cli library --- e2e/nx-plugin/src/nx-plugin.test.ts | 9 ++ .../src/create-nx-plugin.test.ts | 9 +- .../create-nx-plugin/bin/create-nx-plugin.ts | 148 ++++++++++-------- .../src/create-workspace-options.ts | 1 + .../utils/preset/get-third-party-preset.ts | 5 +- packages/nx-plugin/index.ts | 0 .../create-package/create-package.ts | 47 ++++++ .../bin/__name__.ts__tmpl__ | 13 +- .../files/e2e/__name__.spec.ts__tmpl__ | 36 +++++ .../__pluginName__.spec.ts__tmpl__ | 0 .../src/utils/testing-utils/async-commands.ts | 8 +- .../src/utils/testing-utils/commands.ts | 9 +- .../utils/testing-utils/create-package-cli.ts | 33 ++++ .../src/utils/testing-utils/index.ts | 1 + packages/workspace/src/generators/new/new.ts | 15 +- 15 files changed, 244 insertions(+), 90 deletions(-) delete mode 100644 packages/nx-plugin/index.ts rename packages/nx-plugin/src/generators/e2e-project/files/{tests => src}/__pluginName__.spec.ts__tmpl__ (100%) create mode 100644 packages/nx-plugin/src/utils/testing-utils/create-package-cli.ts diff --git a/e2e/nx-plugin/src/nx-plugin.test.ts b/e2e/nx-plugin/src/nx-plugin.test.ts index a1c2c3a06be4a8..1c3d6625384099 100644 --- a/e2e/nx-plugin/src/nx-plugin.test.ts +++ b/e2e/nx-plugin/src/nx-plugin.test.ts @@ -426,4 +426,13 @@ describe('Nx Plugin', () => { `dist/libs/${createAppName}/bin/index.js` ); }); + + it('should throw an error when run create-package for an invalid plugin ', async () => { + const plugin = uniq('plugin'); + expect(() => + runCLI( + `generate @nrwl/nx-plugin:create-package ${plugin} --project=invalid-plugin` + ) + ).toThrow(); + }); }); diff --git a/e2e/workspace-create/src/create-nx-plugin.test.ts b/e2e/workspace-create/src/create-nx-plugin.test.ts index 1412b9e7101fcb..69739d412b19af 100644 --- a/e2e/workspace-create/src/create-nx-plugin.test.ts +++ b/e2e/workspace-create/src/create-nx-plugin.test.ts @@ -13,8 +13,7 @@ describe('create-nx-plugin', () => { afterEach(() => cleanupProject()); - // TODO: Re-enable to work with pnpm - xit('should be able to create a plugin repo and run plugin e2e', () => { + it('should be able to create a plugin repo and run plugin e2e', () => { const wsName = uniq('ws-plugin'); const pluginName = uniq('plugin'); @@ -27,9 +26,13 @@ describe('create-nx-plugin', () => { 'package.json', packageManagerLockFile[packageManager], `packages/${pluginName}/package.json`, - `packages/${pluginName}/project.json` + `packages/${pluginName}/project.json`, + `packages/create-${pluginName}-package/package.json`, + `packages/create-${pluginName}-package/project.json` ); + expect(() => runCLI(`build ${pluginName}`)).not.toThrow(); + expect(() => runCLI(`build create-${pluginName}-package`)).not.toThrow(); expect(() => runCLI(`e2e ${pluginName}-e2e`)).not.toThrow(); }); }); diff --git a/packages/create-nx-plugin/bin/create-nx-plugin.ts b/packages/create-nx-plugin/bin/create-nx-plugin.ts index 0080ab2cc26aad..67ec07d6c11287 100644 --- a/packages/create-nx-plugin/bin/create-nx-plugin.ts +++ b/packages/create-nx-plugin/bin/create-nx-plugin.ts @@ -17,19 +17,18 @@ import { detectInvokedPackageManager, PackageManager, } from './detect-invoked-package-manager'; -import enquirer = require('enquirer'); +import * as enquirer from 'enquirer'; import yargsParser = require('yargs-parser'); const nxVersion = require('../package.json').version; -const tsVersion = 'TYPESCRIPT_VERSION'; // This gets replaced with the typescript version in the root package.json during build -const prettierVersion = 'PRETTIER_VERSION'; // This gets replaced with the prettier version in the root package.json during build -const parsedArgs = yargsParser(process.argv, { - string: ['pluginName', 'packageManager', 'importPath'], +const parsedArgs: yargsParser.Arguments = yargsParser(process.argv, { + string: ['pluginName', 'packageManager', 'importPath', 'createPackageName'], alias: { importPath: 'import-path', pluginName: 'plugin-name', packageManager: 'pm', + createPackageName: 'create-package-name', }, boolean: ['help'], }); @@ -41,8 +40,6 @@ function createSandbox(packageManager: string) { dependencies: { '@nrwl/workspace': nxVersion, nx: nxVersion, - typescript: tsVersion, - prettier: prettierVersion, }, license: 'MIT', }); @@ -55,7 +52,7 @@ function createSandbox(packageManager: string) { return tmpDir; } -function createWorkspace( +function createEmptyWorkspace( tmpDir: string, packageManager: PackageManager, parsedArgs: any, @@ -92,17 +89,25 @@ function createWorkspace( } function createNxPlugin( - workspaceName, - pluginName, - packageManager, - parsedArgs: any + workspaceName: string, + pluginName: string, + packageManager: PackageManager, + createPackageName: string, + parsedArgs: yargsParser.Arguments ) { + const pmc = getPackageManagerCommand(packageManager); + const importPath = parsedArgs.importPath ?? `@${workspaceName}/${pluginName}`; - const command = `nx generate @nrwl/nx-plugin:plugin ${pluginName} --importPath=${importPath}`; - console.log(command); + const generatePluginCommand = `nx generate @nrwl/nx-plugin:plugin ${pluginName} --importPath=${importPath}`; + console.log(generatePluginCommand); + execSync(`${pmc.exec} ${generatePluginCommand}`, { + cwd: workspaceName, + stdio: [0, 1, 2], + }); - const pmc = getPackageManagerCommand(packageManager); - execSync(`${pmc.exec} ${command}`, { + const createPackageCommand = `nx generate @nrwl/nx-plugin:create-package ${createPackageName} --project=${pluginName}`; + console.log(createPackageCommand); + execSync(`${pmc.exec} ${createPackageCommand}`, { cwd: workspaceName, stdio: [0, 1, 2], }); @@ -123,56 +128,52 @@ function updateWorkspace(workspaceName: string) { rmSync(path.join(workspaceName, 'libs'), { recursive: true, force: true }); } -function determineWorkspaceName(parsedArgs: any): Promise { - const workspaceName: string = parsedArgs._[2]; +async function determineWorkspaceName( + parsedArgs: yargsParser.Arguments +): Promise { + const workspaceName = parsedArgs._[2] as string; if (workspaceName) { return Promise.resolve(workspaceName); } - return enquirer - .prompt([ - { - name: 'WorkspaceName', - message: `Workspace name (e.g., org name) `, - type: 'input', - }, - ]) - .then((a: { WorkspaceName: string }) => { - if (!a.WorkspaceName) { - output.error({ - title: 'Invalid workspace name', - bodyLines: [`Workspace name cannot be empty`], - }); - process.exit(1); - } - return a.WorkspaceName; + const results = await enquirer.prompt<{ workspaceName: string }>([ + { + name: 'workspaceName', + message: `Workspace name (e.g., org name) `, + type: 'input', + }, + ]); + if (!results.workspaceName) { + output.error({ + title: 'Invalid workspace name', + bodyLines: [`Workspace name cannot be empty`], }); + process.exit(1); + } + return results.workspaceName; } -function determinePluginName(parsedArgs) { +async function determinePluginName(parsedArgs) { if (parsedArgs.pluginName) { return Promise.resolve(parsedArgs.pluginName); } - return enquirer - .prompt([ - { - name: 'PluginName', - message: `Plugin name `, - type: 'input', - }, - ]) - .then((a: { PluginName: string }) => { - if (!a.PluginName) { - output.error({ - title: 'Invalid name', - bodyLines: [`Name cannot be empty`], - }); - process.exit(1); - } - return a.PluginName; + const results = await enquirer.prompt<{ pluginName: string }>([ + { + name: 'pluginName', + message: `Plugin name `, + type: 'input', + }, + ]); + if (!results.pluginName) { + output.error({ + title: 'Invalid name', + bodyLines: [`Name cannot be empty`], }); + process.exit(1); + } + return results.pluginName; } function showHelp() { @@ -191,21 +192,30 @@ function showHelp() { `); } -if (parsedArgs.help) { - showHelp(); - process.exit(0); +export async function main() { + if (parsedArgs.help) { + showHelp(); + process.exit(0); + } + + const packageManager: PackageManager = + parsedArgs.packageManager || detectInvokedPackageManager(); + const workspaceName = await determineWorkspaceName(parsedArgs); + const pluginName = await determinePluginName(parsedArgs); + const createPackageName = + parsedArgs.createPackageName || `create-${pluginName}-package`; + const tmpDir = createSandbox(packageManager); + createEmptyWorkspace(tmpDir, packageManager, parsedArgs, workspaceName); + updateWorkspace(workspaceName); + createNxPlugin( + workspaceName, + pluginName, + packageManager, + createPackageName, + parsedArgs + ); + await initializeGitRepo(workspaceName); + showNxWarning(workspaceName); } -const packageManager: PackageManager = - parsedArgs.packageManager || detectInvokedPackageManager(); -determineWorkspaceName(parsedArgs).then((workspaceName) => { - return determinePluginName(parsedArgs).then((pluginName) => { - const tmpDir = createSandbox(packageManager); - createWorkspace(tmpDir, packageManager, parsedArgs, workspaceName); - updateWorkspace(workspaceName); - createNxPlugin(workspaceName, pluginName, packageManager, parsedArgs); - return initializeGitRepo(workspaceName).then(() => { - showNxWarning(workspaceName); - }); - }); -}); +main(); diff --git a/packages/create-nx-workspace/src/create-workspace-options.ts b/packages/create-nx-workspace/src/create-workspace-options.ts index e225b19c7ca3fb..b88ca97e436d44 100644 --- a/packages/create-nx-workspace/src/create-workspace-options.ts +++ b/packages/create-nx-workspace/src/create-workspace-options.ts @@ -5,6 +5,7 @@ export interface CreateWorkspaceOptions { name: string; // Workspace name (e.g. org name) packageManager: PackageManager; // Package manager to use nxCloud: boolean; // Enable Nx Cloud + preset: string; // Preset to use /** * @description Enable interactive mode with presets * @default true diff --git a/packages/create-nx-workspace/src/utils/preset/get-third-party-preset.ts b/packages/create-nx-workspace/src/utils/preset/get-third-party-preset.ts index 182bbf30c73585..4b5aa13a6b532d 100644 --- a/packages/create-nx-workspace/src/utils/preset/get-third-party-preset.ts +++ b/packages/create-nx-workspace/src/utils/preset/get-third-party-preset.ts @@ -5,18 +5,19 @@ import { isKnownPreset } from './preset'; /** * This function is used to check if a preset is a third party preset. * @param preset - * @returns null if the preset is a known Nx preset or preset does not exist, the normalized preset otherwise. + * @returns null if the preset is a known Nx preset or preset does not exist, the package name of preset otherwise. */ export async function getThirdPartyPreset( preset?: string ): Promise { if (preset && !isKnownPreset(preset)) { + // extract the package name from the preset const packageName = preset.match(/.+@/) ? preset[0] + preset.substring(1).split('@')[0] : preset; const validateResult = validateNpmPackage(packageName); if (validateResult.validForNewPackages) { - return Promise.resolve(preset); + return Promise.resolve(packageName); } else { //! Error here output.error({ diff --git a/packages/nx-plugin/index.ts b/packages/nx-plugin/index.ts deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/packages/nx-plugin/src/generators/create-package/create-package.ts b/packages/nx-plugin/src/generators/create-package/create-package.ts index e1af7a2030b806..94552e87358d18 100644 --- a/packages/nx-plugin/src/generators/create-package/create-package.ts +++ b/packages/nx-plugin/src/generators/create-package/create-package.ts @@ -36,12 +36,19 @@ export async function createPackageGenerator( ); await createCliPackage(host, options, pluginPackageName); + addTestsToE2eProject(host, options, pluginPackageName); await formatFiles(host); return installTask; } +/** + * Add a preset generator to the plugin if it doesn't exist + * @param host + * @param schema + * @returns package name of the plugin + */ async function addPresetGenerator( host: Tree, schema: NormalizedSchema @@ -80,6 +87,11 @@ async function createCliPackage( packageJson.bin = { [options.name]: './bin/index.js', }; + packageJson.dependencies = { + 'create-nx-workspace': nxVersion, + enquirer: enquirerVersion, + yargs: yargsVersion, + }; host.write(packageJsonPath, JSON.stringify(packageJson)); // update project build target to use the bin entry @@ -92,6 +104,8 @@ async function createCliPackage( options.projectRoot, 'bin/index.ts' ); + projectConfiguration.targets.build.options.buildableProjectDepsInPackageJsonType = + 'dependencies'; updateProjectConfiguration(host, options.projectName, projectConfiguration); // Add bin files to tsconfg.lib.json @@ -112,6 +126,39 @@ async function createCliPackage( ); } +/** + * Add a test file to plugin e2e project + * @param host + * @param options + * @returns + */ +function addTestsToE2eProject( + host: Tree, + options: NormalizedSchema, + pluginPackageName: string +) { + try { + const pluginE2eProjectName = `${options.project}-e2e`; + const projectConfiguration = readProjectConfiguration( + host, + pluginE2eProjectName + ); + generateFiles( + host, + join(__dirname, './files/e2e'), + projectConfiguration.sourceRoot, + { + ...options, + preset: pluginPackageName, + tmpl: '', + } + ); + } catch (e) { + // if e2e project does not exist, do not add tests + return; + } +} + export default createPackageGenerator; export const createPackageSchematic = convertNxGenerator( createPackageGenerator diff --git a/packages/nx-plugin/src/generators/create-package/files/create-framework-app/bin/__name__.ts__tmpl__ b/packages/nx-plugin/src/generators/create-package/files/create-framework-app/bin/__name__.ts__tmpl__ index f2c84707196796..be80c466986864 100644 --- a/packages/nx-plugin/src/generators/create-package/files/create-framework-app/bin/__name__.ts__tmpl__ +++ b/packages/nx-plugin/src/generators/create-package/files/create-framework-app/bin/__name__.ts__tmpl__ @@ -14,9 +14,13 @@ export const commandsObject: yargs.Argv = yargs .command( // this is the default and only command '$0 [name] [options]', - 'Create a new Nx workspace', + 'Create a new <%= preset %> workspace', (yargs) => yargs + .options('preset', { + describe: 'Preset to use', + type: 'string', + }) .option('name', { describe: 'What is the name of your workspace?', type: 'string', @@ -85,7 +89,10 @@ async function normalizeArgsMiddleware(argv: yargs.Arguments) { } async function main(options: Options) { - await createWorkspace('<%= preset %>', options); + // Update below to customize the workspace + await createWorkspace(options.preset ?? '<%= preset %>', options); - console.log(`Successfully created the <%= preset %> workspace: ${options.name}.`); + console.log( + `Successfully created the <%= preset %> workspace: ${options.name}.` + ); } diff --git a/packages/nx-plugin/src/generators/create-package/files/e2e/__name__.spec.ts__tmpl__ b/packages/nx-plugin/src/generators/create-package/files/e2e/__name__.spec.ts__tmpl__ index e69de29bb2d1d6..08e93737f04c00 100644 --- a/packages/nx-plugin/src/generators/create-package/files/e2e/__name__.spec.ts__tmpl__ +++ b/packages/nx-plugin/src/generators/create-package/files/e2e/__name__.spec.ts__tmpl__ @@ -0,0 +1,36 @@ +import { + ensureNxProject, + uniq, + runCreatePackageCli, + runNxCommandAsync +} from '@nrwl/nx-plugin/testing'; + +describe('<%= name %> e2e', () => { + // Setting up individual workspaces per + // test can cause e2e runs to take a long time. + // For this reason, we recommend each suite only + // consumes 1 workspace. The tests should each operate + // on a unique project in the workspace, such that they + // are not dependant on one another. + beforeAll(() => { + ensureNxProject('<%= name %>', 'dist/packages/<%= name %> '); + }); + + afterAll(() => { + // `nx reset` kills the daemon, and performs + // some work which can help clean up e2e leftovers + runNxCommandAsync('reset'); + }); + + it('should run <%= name %>', async () => { + const project = uniq('<%= name %>'); + const result = runCreatePackageCli( + '<%= preset %>', + 'dist/packages/<%= preset %>', + '<%= name %>', + 'dist/packages/<%= name %>', + project + ); + expect(result).toContain('Successfully created'); + }, 120000); +}); diff --git a/packages/nx-plugin/src/generators/e2e-project/files/tests/__pluginName__.spec.ts__tmpl__ b/packages/nx-plugin/src/generators/e2e-project/files/src/__pluginName__.spec.ts__tmpl__ similarity index 100% rename from packages/nx-plugin/src/generators/e2e-project/files/tests/__pluginName__.spec.ts__tmpl__ rename to packages/nx-plugin/src/generators/e2e-project/files/src/__pluginName__.spec.ts__tmpl__ diff --git a/packages/nx-plugin/src/utils/testing-utils/async-commands.ts b/packages/nx-plugin/src/utils/testing-utils/async-commands.ts index 863e2be8b38430..15e270390eb727 100644 --- a/packages/nx-plugin/src/utils/testing-utils/async-commands.ts +++ b/packages/nx-plugin/src/utils/testing-utils/async-commands.ts @@ -11,15 +11,16 @@ import { fileExists } from './utils'; */ export function runCommandAsync( command: string, - opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv } = { + opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv; cwd?: string } = { silenceError: false, + cwd: tmpProjPath(), } ): Promise<{ stdout: string; stderr: string }> { return new Promise((resolve, reject) => { exec( command, { - cwd: tmpProjPath(), + cwd: opts.cwd, env: { ...process.env, ...opts.env }, }, (err, stdout, stderr) => { @@ -39,8 +40,9 @@ export function runCommandAsync( */ export function runNxCommandAsync( command: string, - opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv } = { + opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv; cwd?: string } = { silenceError: false, + cwd: tmpProjPath(), } ): Promise<{ stdout: string; stderr: string }> { if (fileExists(tmpProjPath('package.json'))) { diff --git a/packages/nx-plugin/src/utils/testing-utils/commands.ts b/packages/nx-plugin/src/utils/testing-utils/commands.ts index 7e822968803905..65a4f4aedccee4 100644 --- a/packages/nx-plugin/src/utils/testing-utils/commands.ts +++ b/packages/nx-plugin/src/utils/testing-utils/commands.ts @@ -12,13 +12,14 @@ import { fileExists } from './utils'; */ export function runNxCommand( command?: string, - opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv } = { + opts: { silenceError?: boolean; env?: NodeJS.ProcessEnv; cwd?: string } = { silenceError: false, + cwd: tmpProjPath(), } ): string { function _runNxCommand(c) { const execSyncOptions: ExecOptions = { - cwd: tmpProjPath(), + cwd: opts.cwd, env: { ...process.env, ...opts.env }, }; if (fileExists(tmpProjPath('package.json'))) { @@ -50,11 +51,11 @@ export function runNxCommand( export function runCommand( command: string, - opts?: { env?: NodeJS.ProcessEnv } + opts: { env?: NodeJS.ProcessEnv; cwd?: string } = { cwd: tmpProjPath() } ): string { try { return execSync(command, { - cwd: tmpProjPath(), + cwd: opts.cwd, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, ...opts?.env }, }).toString(); diff --git a/packages/nx-plugin/src/utils/testing-utils/create-package-cli.ts b/packages/nx-plugin/src/utils/testing-utils/create-package-cli.ts new file mode 100644 index 00000000000000..4b132825dcc8e0 --- /dev/null +++ b/packages/nx-plugin/src/utils/testing-utils/create-package-cli.ts @@ -0,0 +1,33 @@ +import { workspaceRoot } from '@nrwl/devkit'; +import { runCommand, runNxCommand } from './commands'; +import { tmpProjPath } from './paths'; + +/** + * This function is used to run the create package CLI command. + * It builds the plugin library and the create package library and run the create package command with for the plugin library. + * It needs to be ran inside an Nx project. It would assume that an Nx project already exists. + * @param pluginLibraryName e.g. my-plugin + * @param pluginLibraryBuildPath e.g. dist/packages/my-plugin + * @param createPackageLibraryName e.g. create-my-plugin-package + * @param createPackageLibraryBuildPath e.g. dist/packages/create-my-plugin-package + * @param projectToBeCreated project name to be created using the cli + * @param packageManager package manager to be used + * @returns results for the create package command + */ +export function runCreatePackageCli( + pluginLibraryName: string, + pluginLibraryBuildPath: string, + createPackageLibraryName: string, + createPackageLibraryBuildPath: string, + projectToBeCreated: string, + packageManager: 'npm' | 'yarn' | 'pnpm' = 'npm' +) { + runNxCommand(`build ${createPackageLibraryName}`, { cwd: process.cwd() }); + return runCommand( + `node ${workspaceRoot}/${createPackageLibraryBuildPath}/bin/index.js ${projectToBeCreated} --preset=${pluginLibraryName}@file:${workspaceRoot}/${pluginLibraryBuildPath} --packageManager=${packageManager} --nxCloud=No` + ); +} + +export function generatedPackagePath(projectToBeCreated: string) { + return `${tmpProjPath}/${projectToBeCreated}`; +} diff --git a/packages/nx-plugin/src/utils/testing-utils/index.ts b/packages/nx-plugin/src/utils/testing-utils/index.ts index 81414ad685bfb5..2541b07012ee3c 100644 --- a/packages/nx-plugin/src/utils/testing-utils/index.ts +++ b/packages/nx-plugin/src/utils/testing-utils/index.ts @@ -1,5 +1,6 @@ export * from './async-commands'; export * from './commands'; +export * from './create-package-cli'; export * from './paths'; export * from './nx-project'; export * from './utils'; diff --git a/packages/workspace/src/generators/new/new.ts b/packages/workspace/src/generators/new/new.ts index 0da28184949714..f2f13109510584 100644 --- a/packages/workspace/src/generators/new/new.ts +++ b/packages/workspace/src/generators/new/new.ts @@ -102,12 +102,15 @@ function normalizeOptions(options: Schema): NormalizedSchema { // If the preset already contains a version in the name // -- my-package@2.0.1 // -- @scope/package@version - const match = options.preset.match( - /^(?(@.+\/)?[^@]+)(@(?\d+\.\d+\.\d+))?$/ - ); - if (match) { - normalized.preset = match.groups.package; - normalized.presetVersion = match.groups.version; + const preset = options.preset; + const packageName = preset.match(/.+@/) + ? preset[0] + preset.substring(1).split('@')[0] + : preset; + if (packageName) { + normalized.preset = packageName; + if (preset.substring(1).split('@').length) { + normalized.presetVersion = preset.substring(1).split('@')[1]; + } } normalized.isCustomPreset = !Object.values(Preset).includes(