diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index d9b7d81b05f859..6615ec173be179 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -5473,6 +5473,14 @@ "isExternal": false, "disableCollapsible": false }, + { + "id": "create-package", + "path": "/packages/nx-plugin/generators/create-package", + "name": "create-package", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, { "id": "e2e-project", "path": "/packages/nx-plugin/generators/e2e-project", diff --git a/docs/generated/manifests/packages.json b/docs/generated/manifests/packages.json index 3f9f14a9f622a7..f062b7f5e14319 100644 --- a/docs/generated/manifests/packages.json +++ b/docs/generated/manifests/packages.json @@ -1920,6 +1920,15 @@ "path": "/packages/nx-plugin/generators/plugin", "type": "generator" }, + "/packages/nx-plugin/generators/create-package": { + "description": "Create a framework package that uses Nx CLI", + "file": "generated/packages/nx-plugin/generators/create-package.json", + "hidden": false, + "name": "create-package", + "originalFilePath": "/packages/nx-plugin/src/generators/create-package/schema.json", + "path": "/packages/nx-plugin/generators/create-package", + "type": "generator" + }, "/packages/nx-plugin/generators/e2e-project": { "description": "Create a E2E application for a Nx Plugin.", "file": "generated/packages/nx-plugin/generators/e2e-project.json", diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index c9faa9146b7747..b3308a40a44ab5 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -1895,6 +1895,15 @@ "path": "nx-plugin/generators/plugin", "type": "generator" }, + { + "description": "Create a framework package that uses Nx CLI", + "file": "generated/packages/nx-plugin/generators/create-package.json", + "hidden": false, + "name": "create-package", + "originalFilePath": "/packages/nx-plugin/src/generators/create-package/schema.json", + "path": "nx-plugin/generators/create-package", + "type": "generator" + }, { "description": "Create a E2E application for a Nx Plugin.", "file": "generated/packages/nx-plugin/generators/e2e-project.json", diff --git a/docs/generated/packages/nx-plugin/generators/create-package.json b/docs/generated/packages/nx-plugin/generators/create-package.json new file mode 100644 index 00000000000000..ec697bc118a999 --- /dev/null +++ b/docs/generated/packages/nx-plugin/generators/create-package.json @@ -0,0 +1,84 @@ +{ + "name": "create-package", + "factory": "./src/generators/create-package/create-package", + "schema": { + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "NxPluginCreatePackage", + "title": "Create a framework package", + "description": "Create a framework package that uses Nx CLI.", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The package name, like `create-framework-app`. Note this must be a valid NPM name to be published.", + "$default": { "$source": "argv", "index": 0 }, + "x-priority": "important" + }, + "project": { + "type": "string", + "description": "The name of the generator project.", + "alias": "p", + "$default": { "$source": "projectName" }, + "x-prompt": "What is the name of the project for the generator?", + "x-priority": "important" + }, + "unitTestRunner": { + "type": "string", + "enum": ["jest", "none"], + "description": "Test runner to use for unit tests.", + "default": "jest" + }, + "directory": { + "type": "string", + "description": "A directory where the app is placed." + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint"], + "default": "eslint" + }, + "tags": { + "type": "string", + "description": "Add tags to the library (used for linting).", + "alias": "t" + }, + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false, + "x-priority": "internal" + }, + "skipTsConfig": { + "type": "boolean", + "default": false, + "description": "Do not update tsconfig.json for development experience.", + "x-priority": "internal" + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint `parserOptions.project` option. We do not do this by default for lint performance reasons.", + "default": false + }, + "compiler": { + "type": "string", + "enum": ["tsc", "swc"], + "default": "tsc", + "description": "The compiler used by the build and test targets." + }, + "importPath": { + "type": "string", + "description": "How the plugin will be published, like `create-framework-app`. Note this must be a valid NPM name. Will use name if not provided." + } + }, + "required": ["name", "project"], + "presets": [] + }, + "description": "Create a framework package that uses Nx CLI", + "implementation": "/packages/nx-plugin/src/generators/create-package/create-package.ts", + "aliases": [], + "hidden": false, + "path": "/packages/nx-plugin/src/generators/create-package/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/nx-plugin/generators/preset.json b/docs/generated/packages/nx-plugin/generators/preset.json index 2253d562ca9d08..bf7c16ce8c2ba6 100644 --- a/docs/generated/packages/nx-plugin/generators/preset.json +++ b/docs/generated/packages/nx-plugin/generators/preset.json @@ -13,6 +13,11 @@ "type": "string", "description": "Plugin name", "aliases": ["name"] + }, + "cliName": { + "type": "string", + "description": "Name of cli command to create workspace with plugin", + "aliases": ["name"] } }, "required": ["pluginName"], diff --git a/docs/generated/packages/workspace/generators/preset.json b/docs/generated/packages/workspace/generators/preset.json index 651d03ffd18cf5..632188aa383caf 100644 --- a/docs/generated/packages/workspace/generators/preset.json +++ b/docs/generated/packages/workspace/generators/preset.json @@ -86,6 +86,7 @@ "default": false } }, + "required": ["preset", "name"], "presets": [] }, "description": "Create application in an empty workspace.", diff --git a/e2e/nx-plugin/src/nx-plugin.test.ts b/e2e/nx-plugin/src/nx-plugin.test.ts index 6988acc304e742..1c3d6625384099 100644 --- a/e2e/nx-plugin/src/nx-plugin.test.ts +++ b/e2e/nx-plugin/src/nx-plugin.test.ts @@ -2,7 +2,6 @@ import { ProjectConfiguration } from '@nrwl/devkit'; import { checkFilesExist, expectTestsPass, - isNotWindows, killPorts, newProject, readJson, @@ -12,8 +11,6 @@ import { uniq, updateFile, createFile, - readFile, - removeFile, cleanupProject, runCommand, getPackageManagerCommand, @@ -411,4 +408,31 @@ describe('Nx Plugin', () => { expect(pluginProject.tags).toEqual(['e2etag', 'e2ePackage']); }, 90000); }); + + it('should be able to generate a create-package plugin ', async () => { + const plugin = uniq('plugin'); + const createAppName = `create-${plugin}-app`; + runCLI(`generate @nrwl/nx-plugin:plugin ${plugin}`); + runCLI( + `generate @nrwl/nx-plugin:create-package ${createAppName} --project=${plugin}` + ); + + const buildResults = runCLI(`build ${createAppName}`); + expect(buildResults).toContain('Done compiling TypeScript files'); + + checkFilesExist( + `libs/${plugin}/src/generators/preset`, + `libs/${createAppName}`, + `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 4611479b83609a..fd3735e02c3c2c 100644 --- a/e2e/workspace-create/src/create-nx-plugin.test.ts +++ b/e2e/workspace-create/src/create-nx-plugin.test.ts @@ -32,5 +32,7 @@ describe('create-nx-plugin', () => { ); expect(() => runCLI(`e2e e2e`)).not.toThrow(); + expect(() => runCLI(`build create-${pluginName}-package`)).not.toThrow(); + expect(() => runCLI(`e2e ${pluginName}`)).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 b3087ec81eb2c1..100c9f8a790b1c 100644 --- a/packages/create-nx-plugin/bin/create-nx-plugin.ts +++ b/packages/create-nx-plugin/bin/create-nx-plugin.ts @@ -65,6 +65,7 @@ function determinePluginName(parsedArgs: CreateNxPluginArguments) { interface CreateNxPluginArguments { pluginName: string; + cliName?: string; packageManager: PackageManager; ci: CI; allPrompts: boolean; @@ -83,11 +84,16 @@ export const commandsObject: yargs.Argv = yargs 'Create a new Nx plugin workspace', (yargs) => withOptions( - yargs.positional('pluginName', { - describe: chalk.dim`Plugin name`, - type: 'string', - alias: ['name'], - }), + yargs + .positional('pluginName', { + describe: chalk.dim`Plugin name`, + type: 'string', + alias: ['name'], + }) + .option('cliName', { + describe: 'Name of the CLI package to create workspace with plugin', + type: 'string', + }), withNxCloud, withCI, withAllPrompts, diff --git a/packages/create-nx-workspace/bin/create-nx-workspace.ts b/packages/create-nx-workspace/bin/create-nx-workspace.ts index 4d17dd28e88576..dd68f33554b9bd 100644 --- a/packages/create-nx-workspace/bin/create-nx-workspace.ts +++ b/packages/create-nx-workspace/bin/create-nx-workspace.ts @@ -188,6 +188,7 @@ async function normalizeArgsMiddleware( } else if (monorepoStyle === 'node-standalone') { preset = Preset.NodeStandalone; } else { + // when choose integrated monorepo, further prompt for preset preset = await determinePreset(argv); } } else if (argv.preset === 'react') { diff --git a/packages/create-nx-workspace/package.json b/packages/create-nx-workspace/package.json index f1d06d338f7bb4..4fdf734b9879df 100644 --- a/packages/create-nx-workspace/package.json +++ b/packages/create-nx-workspace/package.json @@ -27,8 +27,6 @@ "bugs": { "url": "https://github.com/nrwl/nx/issues" }, - "main": "./index.js", - "typings": "./index.d.ts", "homepage": "https://nx.dev", "dependencies": { "chalk": "^4.1.0", diff --git a/packages/create-nx-workspace/project.json b/packages/create-nx-workspace/project.json index aca86797a0e238..c47979257ee8a2 100644 --- a/packages/create-nx-workspace/project.json +++ b/packages/create-nx-workspace/project.json @@ -8,7 +8,7 @@ "build-base": { "executor": "@nrwl/js:tsc", "options": { - "main": "packages/create-nx-workspace/bin/create-nx-workspace.ts", + "main": "packages/create-nx-workspace/index.ts", "assets": [ { "input": "packages/create-nx-workspace", diff --git a/packages/create-nx-workspace/src/create-workspace-options.d.ts b/packages/create-nx-workspace/src/create-workspace-options.ts similarity index 92% rename from packages/create-nx-workspace/src/create-workspace-options.d.ts rename to packages/create-nx-workspace/src/create-workspace-options.ts index c0ecfe190c974c..3e26af8305d2de 100644 --- a/packages/create-nx-workspace/src/create-workspace-options.d.ts +++ b/packages/create-nx-workspace/src/create-workspace-options.ts @@ -1,8 +1,10 @@ import { PackageManager } from './utils/package-manager'; +import { CI } from './utils/ci/ci-list'; export interface CreateWorkspaceOptions { name: string; // Workspace name (e.g. org name) packageManager: PackageManager; // Package manager to use + preset?: string; // Preset to use nxCloud: boolean; // Enable Nx Cloud /** * @description Enable interactive mode with presets diff --git a/packages/create-nx-workspace/src/create-workspace.ts b/packages/create-nx-workspace/src/create-workspace.ts index ed7d090f24f075..2c3e57fe9bf640 100644 --- a/packages/create-nx-workspace/src/create-workspace.ts +++ b/packages/create-nx-workspace/src/create-workspace.ts @@ -64,7 +64,7 @@ export async function createWorkspace( nxCloud && nxCloudInstallRes?.code === 0 ); } - if (!skipGit) { + if (!skipGit && commit) { try { await initializeGitRepo(directory, { defaultBase, commit }); } catch (e) { 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/generators.json b/packages/nx-plugin/generators.json index 31c8ebbe99d89b..d6a77ac3a14e43 100644 --- a/packages/nx-plugin/generators.json +++ b/packages/nx-plugin/generators.json @@ -8,6 +8,11 @@ "schema": "./src/generators/plugin/schema.json", "description": "Create a Nx Plugin." }, + "create-package": { + "factory": "./src/generators/create-package/create-package", + "schema": "./src/generators/create-package/schema.json", + "description": "Create a framework package that uses Nx CLI" + }, "e2e-project": { "factory": "./src/generators/e2e-project/e2e", "schema": "./src/generators/e2e-project/schema.json", @@ -48,6 +53,11 @@ "schema": "./src/generators/plugin/schema.json", "description": "Create a Nx Plugin." }, + "create-package": { + "factory": "./src/generators/create-package/create-package#createPackageSchematic", + "schema": "./src/generators/create-package/schema.json", + "description": "Create a framework package that uses Nx CLI" + }, "e2e-project": { "factory": "./src/generators/e2e-project/e2e#e2eProjectSchematic", "schema": "./src/generators/e2e-project/schema.json", diff --git a/packages/nx-plugin/generators.ts b/packages/nx-plugin/generators.ts index a5c482e0a95801..d2d89360fe8f4b 100644 --- a/packages/nx-plugin/generators.ts +++ b/packages/nx-plugin/generators.ts @@ -1,3 +1,4 @@ +export * from './src/generators/create-package/create-package'; export * from './src/generators/e2e-project/e2e'; export * from './src/generators/executor/executor'; export * from './src/generators/generator/generator'; 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.spec.ts b/packages/nx-plugin/src/generators/create-package/create-package.spec.ts new file mode 100644 index 00000000000000..ca8cdb5ecb6199 --- /dev/null +++ b/packages/nx-plugin/src/generators/create-package/create-package.spec.ts @@ -0,0 +1,125 @@ +import { + joinPathFragments, + readJson, + readProjectConfiguration, + Tree, +} from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { Linter } from '@nrwl/linter'; +import pluginGenerator from '../plugin/plugin'; +import { createPackageGenerator } from './create-package'; +import { CreatePackageSchema } from './schema'; + +const getSchema: ( + overrides?: Partial +) => CreatePackageSchema = (overrides = {}) => ({ + name: 'create-package', + project: 'my-plugin', + compiler: 'tsc', + skipTsConfig: false, + skipFormat: false, + skipLintChecks: false, + linter: Linter.EsLint, + unitTestRunner: 'jest', + minimal: true, + ...overrides, +}); + +describe('NxPlugin Create Package Generator', () => { + let tree: Tree; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + await pluginGenerator(tree, { + name: 'my-plugin', + compiler: 'tsc', + skipTsConfig: false, + skipFormat: false, + skipLintChecks: false, + linter: Linter.EsLint, + unitTestRunner: 'jest', + }); + }); + + it('should update the project.json file', async () => { + await createPackageGenerator(tree, getSchema()); + const project = readProjectConfiguration(tree, 'create-package'); + expect(project.root).toEqual('libs/create-package'); + expect(project.sourceRoot).toEqual('libs/create-package/bin'); + expect(project.targets.build).toEqual({ + executor: '@nrwl/js:tsc', + outputs: ['{options.outputPath}'], + options: { + outputPath: 'dist/libs/create-package', + tsConfig: 'libs/create-package/tsconfig.lib.json', + main: 'libs/create-package/bin/index.ts', + assets: [], + buildableProjectDepsInPackageJsonType: 'dependencies', + }, + }); + }); + + it('should place the create-package plugin in a directory', async () => { + await createPackageGenerator( + tree, + getSchema({ + directory: 'plugins', + } as Partial) + ); + const project = readProjectConfiguration(tree, 'plugins-create-package'); + expect(project.root).toEqual('libs/plugins/create-package'); + }); + + it('should specify tsc as compiler', async () => { + await createPackageGenerator( + tree, + getSchema({ + compiler: 'tsc', + }) + ); + + const { build } = readProjectConfiguration(tree, 'create-package').targets; + + expect(build.executor).toEqual('@nrwl/js:tsc'); + }); + + it('should specify swc as compiler', async () => { + await createPackageGenerator( + tree, + getSchema({ + compiler: 'swc', + }) + ); + + const { build } = readProjectConfiguration(tree, 'create-package').targets; + + expect(build.executor).toEqual('@nrwl/js:swc'); + }); + + it("should use name as default for the package.json's name", async () => { + await createPackageGenerator(tree, getSchema()); + + const { root } = readProjectConfiguration(tree, 'create-package'); + const { name } = readJson<{ name: string }>( + tree, + joinPathFragments(root, 'package.json') + ); + + expect(name).toEqual('create-package'); + }); + + it('should use importPath as the package.json name', async () => { + await createPackageGenerator( + tree, + getSchema({ importPath: '@my-company/create-package' }) + ); + + const { root } = readProjectConfiguration(tree, 'create-package'); + const { name } = readJson<{ name: string }>( + tree, + joinPathFragments(root, 'package.json') + ); + + expect(name).toEqual('@my-company/create-package'); + }); +}); diff --git a/packages/nx-plugin/src/generators/create-package/create-package.ts b/packages/nx-plugin/src/generators/create-package/create-package.ts new file mode 100644 index 00000000000000..9f9c76c9cd3509 --- /dev/null +++ b/packages/nx-plugin/src/generators/create-package/create-package.ts @@ -0,0 +1,170 @@ +import { + addDependenciesToPackageJson, + readProjectConfiguration, + Tree, + generateFiles, + readJson, + convertNxGenerator, + formatFiles, + updateProjectConfiguration, +} from '@nrwl/devkit'; +import { libraryGenerator as jsLibraryGenerator } from '@nrwl/js'; +import { join } from 'path'; +import { nxVersion } from '../../utils/versions'; +import generatorGenerator from '../generator/generator'; +import { CreatePackageSchema } from './schema'; +import { NormalizedSchema, normalizeSchema } from './utils/normalize-schema'; + +const enquirerVersion = '~2.3.6'; +const yargsVersion = '~16.2.0'; + +export async function createPackageGenerator( + host: Tree, + schema: CreatePackageSchema +) { + const options = normalizeSchema(host, schema); + const pluginPackageName = await addPresetGenerator(host, options); + + const installTask = addDependenciesToPackageJson( + host, + {}, + { + 'create-nx-workspace': nxVersion, + enquirer: enquirerVersion, + yargs: yargsVersion, + } + ); + + await createCliPackage(host, options, pluginPackageName); + addTestsToE2eProject(host, options, pluginPackageName); + + if (!options.skipFormat) { + 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 +): Promise { + const { root: projectRoot } = readProjectConfiguration(host, schema.project); + if (!host.exists(`${projectRoot}/src/generators/preset`)) { + await generatorGenerator(host, { + name: 'preset', + project: schema.project, + unitTestRunner: schema.unitTestRunner, + }); + } + + return readJson(host, join(projectRoot, 'package.json'))?.name; +} + +async function createCliPackage( + host: Tree, + options: NormalizedSchema, + pluginPackageName: string +) { + await jsLibraryGenerator(host, { + ...options, + rootProject: false, + config: 'project', + buildable: true, + publishable: true, + bundler: options.bundler, + importPath: options.importPath, + }); + + host.delete(join(options.projectRoot, 'src')); + + // Add the bin entry to the package.json + const packageJsonPath = join(options.projectRoot, 'package.json'); + const packageJson = readJson(host, packageJsonPath); + 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 + const projectConfiguration = readProjectConfiguration( + host, + options.projectName + ); + projectConfiguration.sourceRoot = join(options.projectRoot, 'bin'); + projectConfiguration.targets.build.options.main = join( + options.projectRoot, + 'bin/index.ts' + ); + projectConfiguration.targets.build.options.buildableProjectDepsInPackageJsonType = + 'dependencies'; + updateProjectConfiguration(host, options.projectName, projectConfiguration); + + // Add bin files to tsconfg.lib.json + const tsConfigPath = join(options.projectRoot, 'tsconfig.lib.json'); + const tsConfig = readJson(host, tsConfigPath); + tsConfig.include.push('bin/**/*.ts'); + host.write(tsConfigPath, JSON.stringify(tsConfig)); + + generateFiles( + host, + join(__dirname, './files/create-framework-package'), + options.projectRoot, + { + ...options, + preset: pluginPackageName, + tmpl: '', + } + ); +} + +/** + * 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.rootProject + ? 'e2e' + : `${options.name}-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-package/bin/__name__.ts__tmpl__ b/packages/nx-plugin/src/generators/create-package/files/create-framework-package/bin/__name__.ts__tmpl__ new file mode 100644 index 00000000000000..be80c466986864 --- /dev/null +++ b/packages/nx-plugin/src/generators/create-package/files/create-framework-package/bin/__name__.ts__tmpl__ @@ -0,0 +1,98 @@ +import * as enquirer from 'enquirer'; +import * as yargs from 'yargs'; +import { createWorkspace, CreateWorkspaceOptions } from 'create-nx-workspace'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface Options extends CreateWorkspaceOptions {} + +export const commandsObject: yargs.Argv = yargs + .wrap(yargs.terminalWidth()) + .parserConfiguration({ + 'strip-dashed': true, + 'dot-notation': true, + }) + .command( + // this is the default and only command + '$0 [name] [options]', + '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', + }) + .option('packageManager', { + alias: 'pm', + describe: 'Package manager to use', + choices: ['npm', 'yarn', 'pnpm'], + defaultDescription: 'npm', + type: 'string', + }) + .option('nxCloud', { + describe: 'Enable distributed caching to make your CI faster?', + type: 'boolean', + }), + async (argv: yargs.ArgumentsCamelCase) => { + await main(argv).catch((error) => { + throw error; + }); + }, + [normalizeArgsMiddleware] + ) as yargs.Argv; + +async function normalizeArgsMiddleware(argv: yargs.Arguments) { + if (!argv.name) { + const results = await enquirer.prompt<{ name: string }>({ + type: 'input', + name: 'name', + message: 'What is the name of your workspace?', + }); + argv.name = results.name; + } + if (!argv.packageManager) { + const results = await enquirer.prompt<{ packageManager: string }>({ + name: 'packageManager', + message: 'Which package manager to use', + initial: 'npm' as any, + type: 'autocomplete', + choices: [ + { name: 'npm', message: 'NPM' }, + { name: 'yarn', message: 'Yarn' }, + { name: 'pnpm', message: 'PNPM' }, + ], + }); + argv.packageManager = results.packageManager as any; + } + if (argv.nxCloud === undefined) { + const results = await enquirer.prompt<{ nxCloud: 'Yes' | 'No' }>({ + name: 'NxCloud', + message: 'Enable distributed caching to make your CI faster?', + type: 'autocomplete', + choices: [ + { + name: 'Yes', + hint: 'I want faster builds', + }, + + { + name: 'No', + }, + ], + initial: 'Yes' as any, + }); + argv.nxCloud = results.nxCloud === 'Yes'; + } +} + +async function main(options: Options) { + // Update below to customize the workspace + await createWorkspace(options.preset ?? '<%= preset %>', options); + + console.log( + `Successfully created the <%= preset %> workspace: ${options.name}.` + ); +} diff --git a/packages/nx-plugin/src/generators/create-package/files/create-framework-package/bin/index.ts__tmpl__ b/packages/nx-plugin/src/generators/create-package/files/create-framework-package/bin/index.ts__tmpl__ new file mode 100644 index 00000000000000..0f8d283abcb939 --- /dev/null +++ b/packages/nx-plugin/src/generators/create-package/files/create-framework-package/bin/index.ts__tmpl__ @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { commandsObject } from './<%= name %>'; + +commandsObject.argv; 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__ new file mode 100644 index 00000000000000..95d2ed41429c9b --- /dev/null +++ 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/<%= 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/<%= preset %>', + '<%= name %>', + 'dist/<%= name %>', + project + ); + expect(result).toContain('Successfully created'); + }, 120000); +}); diff --git a/packages/nx-plugin/src/generators/create-package/schema.d.ts b/packages/nx-plugin/src/generators/create-package/schema.d.ts new file mode 100644 index 00000000000000..b1c97caf33079c --- /dev/null +++ b/packages/nx-plugin/src/generators/create-package/schema.d.ts @@ -0,0 +1,16 @@ +export interface CreatePackageSchema { + name: string; + project: string; + + // options to create cli package, passed to js library generator + directory?: string; + skipTsConfig: boolean; + skipFormat: boolean; + tags?: string; + unitTestRunner: 'jest' | 'none'; + linter: Linter; + setParserOptionsProject?: boolean; + compiler: 'swc' | 'tsc'; + importPath?: string; + rootProject?: boolean; +} diff --git a/packages/nx-plugin/src/generators/create-package/schema.json b/packages/nx-plugin/src/generators/create-package/schema.json new file mode 100644 index 00000000000000..79936d0a5c0f00 --- /dev/null +++ b/packages/nx-plugin/src/generators/create-package/schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "NxPluginCreatePackage", + "title": "Create a framework package", + "description": "Create a framework package that uses Nx CLI.", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The package name, like `create-framework-app`. Note this must be a valid NPM name to be published.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-priority": "important" + }, + "project": { + "type": "string", + "description": "The name of the generator project.", + "alias": "p", + "$default": { + "$source": "projectName" + }, + "x-prompt": "What is the name of the project for the generator?", + "x-priority": "important" + }, + "unitTestRunner": { + "type": "string", + "enum": ["jest", "none"], + "description": "Test runner to use for unit tests.", + "default": "jest" + }, + "directory": { + "type": "string", + "description": "A directory where the app is placed." + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint"], + "default": "eslint" + }, + "tags": { + "type": "string", + "description": "Add tags to the library (used for linting).", + "alias": "t" + }, + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false, + "x-priority": "internal" + }, + "skipTsConfig": { + "type": "boolean", + "default": false, + "description": "Do not update tsconfig.json for development experience.", + "x-priority": "internal" + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint `parserOptions.project` option. We do not do this by default for lint performance reasons.", + "default": false + }, + "compiler": { + "type": "string", + "enum": ["tsc", "swc"], + "default": "tsc", + "description": "The compiler used by the build and test targets." + }, + "importPath": { + "type": "string", + "description": "How the plugin will be published, like `create-framework-app`. Note this must be a valid NPM name. Will use name if not provided." + } + }, + "required": ["name", "project"] +} diff --git a/packages/nx-plugin/src/generators/create-package/utils/normalize-schema.ts b/packages/nx-plugin/src/generators/create-package/utils/normalize-schema.ts new file mode 100644 index 00000000000000..d3e613af350b69 --- /dev/null +++ b/packages/nx-plugin/src/generators/create-package/utils/normalize-schema.ts @@ -0,0 +1,46 @@ +import { + extractLayoutDirectory, + getWorkspaceLayout, + joinPathFragments, + names, + Tree, +} from '@nrwl/devkit'; +import { CreatePackageSchema } from '../schema'; + +export interface NormalizedSchema extends CreatePackageSchema { + bundler: 'swc' | 'tsc'; + npmScope: string; + libsDir: string; + projectName: string; + projectRoot: string; + projectDirectory: string; +} + +export function normalizeSchema( + host: Tree, + schema: CreatePackageSchema +): NormalizedSchema { + const { layoutDirectory, projectDirectory } = extractLayoutDirectory( + schema.directory + ); + const { npmScope, libsDir: defaultLibsDir } = getWorkspaceLayout(host); + const libsDir = layoutDirectory ?? defaultLibsDir; + const name = names(schema.name).fileName; + const fullProjectDirectory = projectDirectory + ? `${names(projectDirectory).fileName}/${name}` + : name; + const projectName = fullProjectDirectory.replace(new RegExp('/', 'g'), '-'); + const projectRoot = joinPathFragments(libsDir, fullProjectDirectory); + const importPath = schema.importPath ?? name; + return { + ...schema, + bundler: schema.compiler ?? 'tsc', + npmScope, + libsDir, + projectName, + projectRoot, + name, + projectDirectory: fullProjectDirectory, + importPath, + }; +} diff --git a/packages/nx-plugin/src/generators/e2e-project/e2e.ts b/packages/nx-plugin/src/generators/e2e-project/e2e.ts index ab576298a36f96..6e3ac3d57efc96 100644 --- a/packages/nx-plugin/src/generators/e2e-project/e2e.ts +++ b/packages/nx-plugin/src/generators/e2e-project/e2e.ts @@ -75,7 +75,7 @@ function updateWorkspaceConfiguration(host: Tree, options: NormalizedSchema) { addProjectConfiguration(host, options.projectName, { root: options.projectRoot, projectType: 'application', - sourceRoot: `${options.projectRoot}/src`, + sourceRoot: `${options.projectRoot}/tests`, targets: { e2e: { executor: '@nrwl/nx-plugin:e2e', diff --git a/packages/nx-plugin/src/generators/plugin/plugin.spec.ts b/packages/nx-plugin/src/generators/plugin/plugin.spec.ts index 964adc85bd9ff2..081c873b25acd4 100644 --- a/packages/nx-plugin/src/generators/plugin/plugin.spec.ts +++ b/packages/nx-plugin/src/generators/plugin/plugin.spec.ts @@ -30,7 +30,7 @@ describe('NxPlugin Plugin Generator', () => { tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); }); - it('should update the workspace.json file', async () => { + it('should update the project.json file', async () => { await pluginGenerator(tree, getSchema()); const project = readProjectConfiguration(tree, 'my-plugin'); expect(project.root).toEqual('libs/my-plugin'); diff --git a/packages/nx-plugin/src/generators/plugin/plugin.ts b/packages/nx-plugin/src/generators/plugin/plugin.ts index 6853eb0e8ad3b0..8de933ddb62f4a 100644 --- a/packages/nx-plugin/src/generators/plugin/plugin.ts +++ b/packages/nx-plugin/src/generators/plugin/plugin.ts @@ -134,7 +134,9 @@ export async function pluginGenerator(host: Tree, schema: Schema) { await pluginLintCheckGenerator(host, { projectName: options.name }); } - await formatFiles(host); + if (!options.skipFormat) { + await formatFiles(host); + } return () => installPackagesTask(host); } diff --git a/packages/nx-plugin/src/generators/preset/generator.ts b/packages/nx-plugin/src/generators/preset/generator.ts index 066746ac9b2b68..dc1c724bd00907 100644 --- a/packages/nx-plugin/src/generators/preset/generator.ts +++ b/packages/nx-plugin/src/generators/preset/generator.ts @@ -5,20 +5,23 @@ import { updateJson, updateNxJson, readNxJson, + formatFiles, + runTasksInSerial, } from '@nrwl/devkit'; import { Linter } from '@nrwl/linter'; import { PackageJson } from 'nx/src/utils/package-json'; import { pluginGenerator } from '../plugin/plugin'; import { PresetGeneratorSchema } from './schema'; +import createPackageGenerator from '../create-package/create-package'; export default async function (tree: Tree, options: PresetGeneratorSchema) { - const task = await pluginGenerator(tree, { + const pluginTask = await pluginGenerator(tree, { compiler: 'tsc', linter: Linter.EsLint, name: options.pluginName.includes('/') ? options.pluginName.split('/')[1] : options.pluginName, - skipFormat: false, + skipFormat: true, skipLintChecks: false, skipTsConfig: false, unitTestRunner: 'jest', @@ -29,7 +32,21 @@ export default async function (tree: Tree, options: PresetGeneratorSchema) { removeNpmScope(tree); moveNxPluginToDevDeps(tree); - return task; + const cliTask = await createPackageGenerator(tree, { + name: options.cliName ?? `create-${options.pluginName}-package`, + project: options.pluginName, + skipFormat: true, + skipTsConfig: false, + unitTestRunner: 'jest', + linter: Linter.EsLint, + setParserOptionsProject: false, + compiler: 'tsc', + rootProject: true, + }); + + await formatFiles(tree); + + return runTasksInSerial(pluginTask, cliTask); } function removeNpmScope(tree: Tree) { diff --git a/packages/nx-plugin/src/generators/preset/schema.d.ts b/packages/nx-plugin/src/generators/preset/schema.d.ts index bff8df3c05dfbc..897d0b7e10c5a6 100644 --- a/packages/nx-plugin/src/generators/preset/schema.d.ts +++ b/packages/nx-plugin/src/generators/preset/schema.d.ts @@ -1,3 +1,4 @@ export interface PresetGeneratorSchema { pluginName: string; + cliName?: string; } diff --git a/packages/nx-plugin/src/generators/preset/schema.json b/packages/nx-plugin/src/generators/preset/schema.json index bc224d8091d579..edb0cdf69a84b6 100644 --- a/packages/nx-plugin/src/generators/preset/schema.json +++ b/packages/nx-plugin/src/generators/preset/schema.json @@ -10,6 +10,11 @@ "type": "string", "description": "Plugin name", "aliases": ["name"] + }, + "cliName": { + "type": "string", + "description": "Name of cli command to create workspace with plugin", + "aliases": ["name"] } }, "required": ["pluginName"] 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..c1171e82ad1e79 --- /dev/null +++ b/packages/nx-plugin/src/utils/testing-utils/create-package-cli.ts @@ -0,0 +1,38 @@ +import { + detectPackageManager, + PackageManager, + 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?: PackageManager +) { + packageManager = packageManager ?? detectPackageManager(process.cwd()); + 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/react/src/module-federation/with-module-federation.ts b/packages/react/src/module-federation/with-module-federation.ts index f7213f2caee480..c1a5f56336cb5c 100644 --- a/packages/react/src/module-federation/with-module-federation.ts +++ b/packages/react/src/module-federation/with-module-federation.ts @@ -2,7 +2,7 @@ import { ModuleFederationConfig } from '@nrwl/devkit'; import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph'; import { getModuleFederationConfig } from './utils'; import ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); -import type { AsyncNxWebpackPlugin, NxWebpackPlugin } from '@nrwl/webpack'; +import type { AsyncNxWebpackPlugin } from '@nrwl/webpack'; function determineRemoteUrl(remote: string) { const remoteConfiguration = readCachedProjectConfiguration(remote); 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( diff --git a/packages/workspace/src/generators/preset/schema.json b/packages/workspace/src/generators/preset/schema.json index ca6f3c5830df8d..fe8bd6cc9d51eb 100644 --- a/packages/workspace/src/generators/preset/schema.json +++ b/packages/workspace/src/generators/preset/schema.json @@ -88,5 +88,6 @@ "type": "boolean", "default": false } - } + }, + "required": ["preset", "name"] }