diff --git a/.cz-config.js b/.cz-config.js index 2efae0e2ef..7b7904faa0 100644 --- a/.cz-config.js +++ b/.cz-config.js @@ -21,6 +21,7 @@ module.exports = { { name: 'docs', description: 'anything related to docs infrastructure' }, { name: 'nextjs', description: 'anything Next specific' }, { name: 'node', description: 'anything Node specific' }, + { name: 'nx-plugin', description: 'anything Nx Plugin specific' }, { name: 'react', description: 'anything React specific' }, { name: 'storybook', description: 'anything Storybook specific' }, { diff --git a/docs/angular/api-nx-plugin/builders/e2e.md b/docs/angular/api-nx-plugin/builders/e2e.md new file mode 100644 index 0000000000..ff930503cd --- /dev/null +++ b/docs/angular/api-nx-plugin/builders/e2e.md @@ -0,0 +1,25 @@ +# e2e + +Creates and runs an e2e for a Nx Plugin + +Builder properties can be configured in angular.json when defining the builder, or when invoking it. + +## Properties + +### jestConfig + +Type: `string` + +Jest config file + +### target + +Type: `string` + +the target Nx Plugin project and build + +### tsSpecConfig + +Type: `string` + +Spec tsconfig file diff --git a/docs/angular/api-nx-plugin/schematics/plugin.md b/docs/angular/api-nx-plugin/schematics/plugin.md new file mode 100644 index 0000000000..8f4be6beb6 --- /dev/null +++ b/docs/angular/api-nx-plugin/schematics/plugin.md @@ -0,0 +1,91 @@ +# plugin + +Create a Nx Plugin + +## Usage + +```bash +ng generate plugin ... +``` + +By default, Nx will search for `plugin` in the default collection provisioned in `angular.json`. + +You can specify the collection explicitly as follows: + +```bash +ng g @nrwl/nx-plugin:plugin ... +``` + +Show what will be generated without writing to disk: + +```bash +ng g plugin ... --dry-run +``` + +### Examples + +Generate libs/plugins/my-plugin: + +```bash +ng g plugin my-plugin --directory=plugins +``` + +## Options + +### directory + +Alias(es): d + +Type: `string` + +A directory where the plugin is placed + +### linter + +Default: `tslint` + +Type: `string` + +Possible values: `eslint`, `tslint` + +The tool to use for running lint checks. + +### name + +Type: `string` + +Plugin name + +### skipFormat + +Default: `false` + +Type: `boolean` + +Skip formatting files + +### skipTsConfig + +Default: `false` + +Type: `boolean` + +Do not update tsconfig.json for development experience. + +### tags + +Alias(es): t + +Type: `string` + +Add tags to the library (used for linting) + +### unitTestRunner + +Default: `jest` + +Type: `string` + +Possible values: `jest`, `none` + +Test runner to use for unit tests diff --git a/docs/angular/builders.json b/docs/angular/builders.json index 745a12ae2e..e7b952f1a6 100644 --- a/docs/angular/builders.json +++ b/docs/angular/builders.json @@ -6,6 +6,7 @@ "nest", "next", "node", + "nx-plugin", "storybook", "web", "workspace" diff --git a/docs/angular/schematics.json b/docs/angular/schematics.json index ada5ef03fd..8676bdea5b 100644 --- a/docs/angular/schematics.json +++ b/docs/angular/schematics.json @@ -7,6 +7,7 @@ "nest", "next", "node", + "nx-plugin", "react", "storybook", "web", diff --git a/docs/react/api-nx-plugin/builders/e2e.md b/docs/react/api-nx-plugin/builders/e2e.md new file mode 100644 index 0000000000..a3785bbd7d --- /dev/null +++ b/docs/react/api-nx-plugin/builders/e2e.md @@ -0,0 +1,26 @@ +# e2e + +Creates and runs an e2e for a Nx Plugin + +Builder properties can be configured in workspace.json when defining the builder, or when invoking it. +Read more about how to use builders and the CLI here: https://nx.dev/react/guides/cli. + +## Properties + +### jestConfig + +Type: `string` + +Jest config file + +### target + +Type: `string` + +the target Nx Plugin project and build + +### tsSpecConfig + +Type: `string` + +Spec tsconfig file diff --git a/docs/react/api-nx-plugin/schematics/plugin.md b/docs/react/api-nx-plugin/schematics/plugin.md new file mode 100644 index 0000000000..83bfe7d7ef --- /dev/null +++ b/docs/react/api-nx-plugin/schematics/plugin.md @@ -0,0 +1,91 @@ +# plugin + +Create a Nx Plugin + +## Usage + +```bash +nx generate plugin ... +``` + +By default, Nx will search for `plugin` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/nx-plugin:plugin ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g plugin ... --dry-run +``` + +### Examples + +Generate libs/plugins/my-plugin: + +```bash +nx g plugin my-plugin --directory=plugins +``` + +## Options + +### directory + +Alias(es): d + +Type: `string` + +A directory where the plugin is placed + +### linter + +Default: `tslint` + +Type: `string` + +Possible values: `eslint`, `tslint` + +The tool to use for running lint checks. + +### name + +Type: `string` + +Plugin name + +### skipFormat + +Default: `false` + +Type: `boolean` + +Skip formatting files + +### skipTsConfig + +Default: `false` + +Type: `boolean` + +Do not update tsconfig.json for development experience. + +### tags + +Alias(es): t + +Type: `string` + +Add tags to the library (used for linting) + +### unitTestRunner + +Default: `jest` + +Type: `string` + +Possible values: `jest`, `none` + +Test runner to use for unit tests diff --git a/docs/react/builders.json b/docs/react/builders.json index 745a12ae2e..e7b952f1a6 100644 --- a/docs/react/builders.json +++ b/docs/react/builders.json @@ -6,6 +6,7 @@ "nest", "next", "node", + "nx-plugin", "storybook", "web", "workspace" diff --git a/docs/react/schematics.json b/docs/react/schematics.json index ada5ef03fd..8676bdea5b 100644 --- a/docs/react/schematics.json +++ b/docs/react/schematics.json @@ -7,6 +7,7 @@ "nest", "next", "node", + "nx-plugin", "react", "storybook", "web", diff --git a/docs/web/api-nx-plugin/builders/e2e.md b/docs/web/api-nx-plugin/builders/e2e.md new file mode 100644 index 0000000000..76ea2c2144 --- /dev/null +++ b/docs/web/api-nx-plugin/builders/e2e.md @@ -0,0 +1,26 @@ +# e2e + +Creates and runs an e2e for a Nx Plugin + +Builder properties can be configured in workspace.json when defining the builder, or when invoking it. +Read more about how to use builders and the CLI here: https://nx.dev/web/guides/cli. + +## Properties + +### jestConfig + +Type: `string` + +Jest config file + +### target + +Type: `string` + +the target Nx Plugin project and build + +### tsSpecConfig + +Type: `string` + +Spec tsconfig file diff --git a/docs/web/api-nx-plugin/schematics/plugin.md b/docs/web/api-nx-plugin/schematics/plugin.md new file mode 100644 index 0000000000..83bfe7d7ef --- /dev/null +++ b/docs/web/api-nx-plugin/schematics/plugin.md @@ -0,0 +1,91 @@ +# plugin + +Create a Nx Plugin + +## Usage + +```bash +nx generate plugin ... +``` + +By default, Nx will search for `plugin` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/nx-plugin:plugin ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g plugin ... --dry-run +``` + +### Examples + +Generate libs/plugins/my-plugin: + +```bash +nx g plugin my-plugin --directory=plugins +``` + +## Options + +### directory + +Alias(es): d + +Type: `string` + +A directory where the plugin is placed + +### linter + +Default: `tslint` + +Type: `string` + +Possible values: `eslint`, `tslint` + +The tool to use for running lint checks. + +### name + +Type: `string` + +Plugin name + +### skipFormat + +Default: `false` + +Type: `boolean` + +Skip formatting files + +### skipTsConfig + +Default: `false` + +Type: `boolean` + +Do not update tsconfig.json for development experience. + +### tags + +Alias(es): t + +Type: `string` + +Add tags to the library (used for linting) + +### unitTestRunner + +Default: `jest` + +Type: `string` + +Possible values: `jest`, `none` + +Test runner to use for unit tests diff --git a/docs/web/builders.json b/docs/web/builders.json index 745a12ae2e..e7b952f1a6 100644 --- a/docs/web/builders.json +++ b/docs/web/builders.json @@ -6,6 +6,7 @@ "nest", "next", "node", + "nx-plugin", "storybook", "web", "workspace" diff --git a/docs/web/schematics.json b/docs/web/schematics.json index ada5ef03fd..8676bdea5b 100644 --- a/docs/web/schematics.json +++ b/docs/web/schematics.json @@ -7,6 +7,7 @@ "nest", "next", "node", + "nx-plugin", "react", "storybook", "web", diff --git a/e2e/nx-plugin.test.ts b/e2e/nx-plugin.test.ts new file mode 100644 index 0000000000..ef0ecd0b6e --- /dev/null +++ b/e2e/nx-plugin.test.ts @@ -0,0 +1,97 @@ +import { + forEachCli, + ensureProject, + uniq, + runCLI, + updateFile, + expectTestsPass, + runCLIAsync, + checkFilesExist, + readJson, + workspaceConfigName +} from './utils'; + +forEachCli(currentCLIName => { + const linter = currentCLIName === 'angular' ? 'tslint' : 'eslint'; + + describe('Nx Plugin', () => { + it('should be able to generate a Nx Plugin ', async done => { + ensureProject(); + const plugin = uniq('plugin'); + + runCLI(`generate @nrwl/nx-plugin:plugin ${plugin} --linter=${linter}`); + const lintResults = runCLI(`lint ${plugin}`); + expect(lintResults).toContain('All files pass linting.'); + + expectTestsPass(await runCLIAsync(`test ${plugin}`)); + + const buildResults = runCLI(`build ${plugin}`); + expect(buildResults).toContain('Done compiling TypeScript files'); + checkFilesExist( + `dist/libs/${plugin}/package.json`, + `dist/libs/${plugin}/collection.json`, + `dist/libs/${plugin}/builders.json`, + `dist/libs/${plugin}/src/index.js`, + `dist/libs/${plugin}/src/schematics/${plugin}/schema.json`, + `dist/libs/${plugin}/src/schematics/${plugin}/schema.d.ts`, + `dist/libs/${plugin}/src/schematics/${plugin}/schematic.js`, + `dist/libs/${plugin}/src/schematics/${plugin}/files/src/index.ts.template`, + `dist/libs/${plugin}/src/builders/${plugin}/builder.js`, + `dist/libs/${plugin}/src/builders/${plugin}/schema.d.ts`, + `dist/libs/${plugin}/src/builders/${plugin}/schema.json` + ); + const nxJson = readJson('nx.json'); + expect(nxJson).toMatchObject({ + projects: expect.objectContaining({ + [plugin]: { + tags: [] + }, + [`${plugin}-e2e`]: { + tags: [], + implicitDependencies: [`${plugin}`] + } + }) + }); + done(); + }, 45000); + + it(`should run the plugin's e2e tests`, async done => { + ensureProject(); + const plugin = uniq('plugin'); + runCLI(`generate @nrwl/nx-plugin:plugin ${plugin} --linter=${linter}`); + const results = await runCLIAsync(`e2e ${plugin}-e2e`); + expect(results.stdout).toContain('Compiling TypeScript files'); + expectTestsPass(results); + + done(); + }, 75000); + + describe('--directory', () => { + it('should create a plugin in the specified directory', () => { + ensureProject(); + const plugin = uniq('plugin'); + runCLI( + `generate @nrwl/nx-plugin:plugin ${plugin} --linter=${linter} --directory subdir` + ); + checkFilesExist(`libs/subdir/${plugin}/package.json`); + const workspace = readJson(workspaceConfigName()); + expect(workspace.projects[`subdir-${plugin}`]).toBeTruthy(); + expect(workspace.projects[`subdir-${plugin}`].root).toBe( + `libs/subdir/${plugin}` + ); + expect(workspace.projects[`subdir-${plugin}-e2e`]).toBeTruthy(); + }, 45000); + }); + describe('--tags', () => { + it('should add tags to nx.json', async () => { + ensureProject(); + const plugin = uniq('plugin'); + runCLI( + `generate @nrwl/nx-plugin:plugin ${plugin} --linter=${linter} --tags=e2etag,e2ePackage` + ); + const nxJson = readJson('nx.json'); + expect(nxJson.projects[plugin].tags).toEqual(['e2etag', 'e2ePackage']); + }, 45000); + }); + }); +}); diff --git a/package.json b/package.json index d4ff58a73f..34e024b65a 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "@testing-library/react": "9.4.0", "@types/express": "4.17.0", "@types/fast-levenshtein": "^0.0.1", + "@types/fs-extra": "7.0.0", "@types/jasmine": "~2.8.6", "@types/jasminewd2": "~2.0.3", "@types/jest": "24.0.9", diff --git a/packages/node/src/builders/package/package.impl.spec.ts b/packages/node/src/builders/package/package.impl.spec.ts index 4a8ce2f65b..4f73e2b296 100644 --- a/packages/node/src/builders/package/package.impl.spec.ts +++ b/packages/node/src/builders/package/package.impl.spec.ts @@ -104,6 +104,18 @@ describe('NodeCompileBuilder', () => { fakeEventEmitter.emit('exit', 0); }); + it('should have the output path in the BuilderOutput', done => { + runNodePackageBuilder(testOptions, context).subscribe({ + next: value => { + expect(value.outputPath).toEqual(testOptions.outputPath); + }, + complete: () => { + done(); + } + }); + fakeEventEmitter.emit('exit', 0); + }); + describe('Asset copying', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/node/src/builders/package/package.impl.ts b/packages/node/src/builders/package/package.impl.ts index 7140fbe15d..e5c5e18916 100644 --- a/packages/node/src/builders/package/package.impl.ts +++ b/packages/node/src/builders/package/package.impl.ts @@ -11,7 +11,7 @@ import { copy, removeSync } from 'fs-extra'; import * as glob from 'glob'; import { basename, dirname, join, normalize, relative } from 'path'; import { Observable, Subscriber } from 'rxjs'; -import { switchMap, tap } from 'rxjs/operators'; +import { switchMap, tap, map } from 'rxjs/operators'; import * as treeKill from 'tree-kill'; export interface NodePackageBuilderOptions extends JsonObject { @@ -26,6 +26,7 @@ export interface NodePackageBuilderOptions extends JsonObject { interface NormalizedBuilderOptions extends NodePackageBuilderOptions { files: Array; + normalizedOutputPath: string; relativeMainFileOutput: string; } @@ -51,6 +52,12 @@ export function runNodePackageBuilder( }), switchMap(() => { return copyAssetFiles(normalizedOptions, context); + }), + map(value => { + return { + ...value, + outputPath: normalizedOptions.outputPath + }; }) ); } @@ -109,7 +116,8 @@ function normalizeOptions( return { ...options, files, - relativeMainFileOutput + relativeMainFileOutput, + normalizedOutputPath: join(context.workspaceRoot, options.outputPath) }; } @@ -122,7 +130,7 @@ function compileTypeScriptFiles( killProcess(context); } // Cleaning the /dist folder - removeSync(join(context.workspaceRoot, options.outputPath)); + removeSync(options.normalizedOutputPath); return Observable.create((subscriber: Subscriber) => { try { @@ -130,7 +138,7 @@ function compileTypeScriptFiles( '-p', join(context.workspaceRoot, options.tsConfig), '--outDir', - join(context.workspaceRoot, options.outputPath) + options.normalizedOutputPath ]; if (options.sourceMap) { diff --git a/packages/nx-plugin/bin/create.ts b/packages/nx-plugin/bin/create.ts new file mode 100644 index 0000000000..3035b9145b --- /dev/null +++ b/packages/nx-plugin/bin/create.ts @@ -0,0 +1,248 @@ +#!/usr/bin/env node + +// we can import from '@nrwl/workspace' because it will require typescript +import { output } from '@nrwl/workspace/src/utils/output'; +import { dirSync } from 'tmp'; +import { writeFileSync } from 'fs-extra'; +import * as path from 'path'; +import { execSync } from 'child_process'; +import * as inquirer from 'inquirer'; +import yargsParser = require('yargs-parser'); + +const tsVersion = 'TYPESCRIPT_VERSION'; +const cliVersion = 'NX_VERSION'; +const nxVersion = 'NX_VERSION'; +const prettierVersion = 'PRETTIER_VERSION'; + +const parsedArgs = yargsParser(process.argv, { + string: ['pluginName'], + alias: { + pluginName: 'plugin-name' + }, + boolean: ['help'] +}); + +if (parsedArgs.help) { + showHelp(); + process.exit(0); +} + +const packageManager = determinePackageManager(); +determineWorkspaceName(parsedArgs).then(workspaceName => { + return determinPluginName(parsedArgs).then(pluginName => { + const tmpDir = createSandbox(packageManager); + createWorkspace(tmpDir, packageManager, parsedArgs, workspaceName); + createNxPlugin(workspaceName, pluginName); + commitChanges(workspaceName); + showNxWarning(workspaceName); + }); +}); + +function createSandbox(packageManager: string) { + console.log(`Creating a sandbox with Nx...`); + const tmpDir = dirSync().name; + writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + dependencies: { + '@nrwl/workspace': nxVersion, + '@nrwl/tao': cliVersion, + typescript: tsVersion, + prettier: prettierVersion + }, + license: 'MIT' + }) + ); + + execSync(`${packageManager} install --silent`, { + cwd: tmpDir, + stdio: [0, 1, 2] + }); + + return tmpDir; +} + +function createWorkspace( + tmpDir: string, + packageManager: string, + parsedArgs: any, + name: string +) { + const args = [ + name, + ...process.argv.slice(parsedArgs._[2] ? 3 : 2).map(a => `"${a}"`) + ].join(' '); + + console.log(`new ${args} --preset=empty --collection=@nrwl/workspace`); + execSync( + `"${path.join( + tmpDir, + 'node_modules', + '.bin', + 'tao' + )}" new ${args} --preset=empty --collection=@nrwl/workspace`, + { + stdio: [0, 1, 2] + } + ); + execSync(`${packageManager} add -D @nrwl/nx-plugin@${nxVersion}`, { + cwd: name, + stdio: [0, 1, 2] + }); +} + +function createNxPlugin(workspaceName, pluginName) { + console.log(`nx generate @nrwl/nx-plugin:plugin ${pluginName}`); + execSync( + `node ./node_modules/@nrwl/cli/bin/nx.js generate @nrwl/nx-plugin:plugin ${pluginName}`, + { + cwd: workspaceName, + stdio: [0, 1, 2] + } + ); +} + +function commitChanges(workspaceName) { + execSync('git add .', { + cwd: workspaceName, + stdio: 'ignore' + }); + execSync('git commit --amend --no-edit', { + cwd: workspaceName, + stdio: 'ignore' + }); +} + +function showNxWarning(workspaceName: string) { + try { + const pathToRunNxCommand = path.resolve(process.cwd(), workspaceName); + execSync('nx --version', { + cwd: pathToRunNxCommand, + stdio: ['ignore', 'ignore', 'ignore'] + }); + } catch (e) { + // no nx found + output.addVerticalSeparator(); + output.note({ + title: `Nx CLI is not installed globally.`, + bodyLines: [ + `This means that you might have to use "yarn nx" or "npm nx" to execute commands in the workspace.`, + `Run "yarn global add @nrwl/cli" or "npm install -g @nrwl/cli" to be able to execute command directly.` + ] + }); + } +} + +// Move to @nrwl/workspace package? +function determinePackageManager() { + let packageManager = getPackageManagerFromAngularCLI(); + if (packageManager === 'npm' || isPackageManagerInstalled(packageManager)) { + return packageManager; + } + + if (isPackageManagerInstalled('yarn')) { + return 'yarn'; + } + + if (isPackageManagerInstalled('pnpm')) { + return 'pnpm'; + } + + return 'npm'; +} + +function getPackageManagerFromAngularCLI(): string { + // If you have Angular CLI installed, read Angular CLI config. + // If it isn't installed, default to 'yarn'. + try { + return execSync('ng config -g cli.packageManager', { + stdio: ['ignore', 'pipe', 'ignore'], + timeout: 500 + }) + .toString() + .trim(); + } catch (e) { + return 'yarn'; + } +} + +function isPackageManagerInstalled(packageManager: string) { + let isInstalled = false; + try { + execSync(`${packageManager} --version`, { + stdio: ['ignore', 'ignore', 'ignore'] + }); + isInstalled = true; + } catch (e) { + /* do nothing */ + } + return isInstalled; +} + +function determineWorkspaceName(parsedArgs: any): Promise { + const workspaceName: string = parsedArgs._[2]; + + if (workspaceName) { + return Promise.resolve(workspaceName); + } + + return inquirer + .prompt([ + { + name: 'WorkspaceName', + message: `Workspace name (e.g., org name) `, + type: 'string' + } + ]) + .then(a => { + if (!a.WorkspaceName) { + output.error({ + title: 'Invalid workspace name', + bodyLines: [`Workspace name cannot be empty`] + }); + process.exit(1); + } + return a.WorkspaceName; + }); +} + +function determinPluginName(parsedArgs) { + if (parsedArgs.pluginName) { + return Promise.resolve(parsedArgs.pluginName); + } + + return inquirer + .prompt([ + { + name: 'PluginName', + message: `Plugin name `, + type: 'string' + } + ]) + .then(a => { + if (!a.PluginName) { + output.error({ + title: 'Invalid name', + bodyLines: [`Name cannot be empty`] + }); + process.exit(1); + } + return a.PluginName; + }); +} + +function showHelp() { + console.log(` + Usage: [options] + + Create a new Nx workspace + + Args: + + name workspace name + + Options: + + pluginName the name of the plugin to be created +`); +} diff --git a/packages/nx-plugin/builders.json b/packages/nx-plugin/builders.json new file mode 100644 index 0000000000..757bbb4509 --- /dev/null +++ b/packages/nx-plugin/builders.json @@ -0,0 +1,10 @@ +{ + "$schema": "@angular-devkit/architect/src/builders-schema.json", + "builders": { + "e2e": { + "implementation": "./src/builders/e2e/e2e.impl", + "schema": "./src/builders/e2e/schema.json", + "description": "Creates and runs an e2e for a Nx Plugin" + } + } +} diff --git a/packages/nx-plugin/collection.json b/packages/nx-plugin/collection.json new file mode 100644 index 0000000000..0df46231f4 --- /dev/null +++ b/packages/nx-plugin/collection.json @@ -0,0 +1,18 @@ +{ + "name": "Nx Plugin", + "version": "0.1", + "extends": ["@nrwl/workspace"], + "schematics": { + "plugin": { + "factory": "./src/schematics/plugin/plugin", + "schema": "./src/schematics/plugin/schema.json", + "description": "Create a Nx Plugin" + }, + "e2e-project": { + "factory": "./src/schematics/e2e-project/e2e", + "schema": "./src/schematics/e2e-project/schema.json", + "description": "Create a e2e application for a Nx Plugin", + "hidden": true + } + } +} diff --git a/packages/nx-plugin/index.ts b/packages/nx-plugin/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/nx-plugin/migrations.json b/packages/nx-plugin/migrations.json new file mode 100644 index 0000000000..63001b4458 --- /dev/null +++ b/packages/nx-plugin/migrations.json @@ -0,0 +1,3 @@ +{ + "schematics": {} +} diff --git a/packages/nx-plugin/package.json b/packages/nx-plugin/package.json new file mode 100644 index 0000000000..1be6af9a67 --- /dev/null +++ b/packages/nx-plugin/package.json @@ -0,0 +1,49 @@ +{ + "name": "@nrwl/nx-plugin", + "version": "0.0.1", + "description": "Node Plugin for Nx", + "repository": { + "type": "git", + "url": "git+https://github.com/nrwl/nx.git" + }, + "keywords": [ + "Monorepo", + "Node", + "Nest", + "Jest", + "Cypress", + "CLI" + ], + "main": "./index.js", + "types": "./index.d.ts", + "bin": "./bin/create.js", + "author": "Nrwl", + "license": "MIT", + "bugs": { + "url": "https://github.com/nrwl/nx/issues" + }, + "homepage": "https://nx.dev", + "schematics": "./collection.json", + "builders": "./builders.json", + "ng-update": { + "requirements": {}, + "migrations": "./migrations.json" + }, + "peerDependencies": { + "@nrwl/workspace": "*" + }, + "dependencies": { + "@nrwl/node": "*", + "@nrwl/linter": "*", + "@nrwl/workspace": "*", + "@nrwl/tao": "*", + "@angular-devkit/architect": "0.803.23", + "@angular-devkit/core": "8.3.23", + "@angular-devkit/schematics": "8.3.23", + "fs-extra": "7.0.1", + "tmp": "0.0.33", + "yargs-parser": "10.0.0", + "yargs": "^11.0.0", + "inquirer": "^6.3.1" + } +} diff --git a/packages/nx-plugin/src/builders/e2e/e2e.impl.spec.ts b/packages/nx-plugin/src/builders/e2e/e2e.impl.spec.ts new file mode 100644 index 0000000000..8df0ba0870 --- /dev/null +++ b/packages/nx-plugin/src/builders/e2e/e2e.impl.spec.ts @@ -0,0 +1,64 @@ +import { NxPluginE2EBuilderOptions, runNxPluginE2EBuilder } from './e2e.impl'; +import { MockBuilderContext } from '@nrwl/workspace/testing'; +import { getMockContext } from '../../utils/testing'; +import * as devkitArchitect from '@angular-devkit/architect'; +import { of } from 'rxjs'; + +describe('NxPluginE2EBuilder', () => { + let testOptions: NxPluginE2EBuilderOptions; + let context: MockBuilderContext; + let scheduleTargetAndForgetSpy: jest.SpyInstance; + let contextBuilderSpy: jest.SpyInstance; + beforeEach(async () => { + context = await getMockContext(); + context.addTarget( + { project: 'plugin-e2e', target: 'build' }, + '@nrwl/nx-plugin:e2e' + ); + testOptions = { + jestConfig: 'apps/plugin-e2e/jest.config.js', + tsSpecConfig: 'apps/plugin-e2e/tsconfig.spec.js', + target: 'plugin:build' + }; + + scheduleTargetAndForgetSpy = jest + .spyOn(devkitArchitect, 'scheduleTargetAndForget') + .mockImplementation((context, options) => { + debugger; + return of({ success: true }); + }); + + contextBuilderSpy = jest + .spyOn(context, 'scheduleBuilder') + .mockImplementation((name, overrides) => { + console.log('hello'); + return new Promise((res, rej) => { + res({ + result: of({ success: true }).toPromise(), + id: 1, + info: { + builderName: 'builder', + description: '', + optionSchema: {} + }, + output: of({ success: true }), + progress: of({} as any), + stop: jest.fn() + }); + }); + }); + }); + + it('should build the plugin and run the test', async () => { + await runNxPluginE2EBuilder(testOptions, context).toPromise(); + expect(scheduleTargetAndForgetSpy).toHaveBeenCalledWith(context, { + project: 'plugin', + target: 'build' + }); + expect(contextBuilderSpy).toHaveBeenCalledWith('@nrwl/jest:jest', { + tsConfig: testOptions.tsSpecConfig, + jestConfig: testOptions.jestConfig, + watch: false + }); + }); +}); diff --git a/packages/nx-plugin/src/builders/e2e/e2e.impl.ts b/packages/nx-plugin/src/builders/e2e/e2e.impl.ts new file mode 100644 index 0000000000..4d1ea0f1c8 --- /dev/null +++ b/packages/nx-plugin/src/builders/e2e/e2e.impl.ts @@ -0,0 +1,37 @@ +import { + BuilderContext, + createBuilder, + scheduleTargetAndForget, + targetFromTargetString +} from '@angular-devkit/architect'; +import { switchMap, concatMap } from 'rxjs/operators'; +import { Schema } from './schema'; +import { from } from 'rxjs'; + +try { + require('dotenv').config(); +} catch (e) {} + +export interface NxPluginE2EBuilderOptions extends Schema {} + +export default createBuilder(runNxPluginE2EBuilder); +export function runNxPluginE2EBuilder( + options: NxPluginE2EBuilderOptions, + context: BuilderContext +) { + return buildTarget(context, options.target).pipe( + switchMap(() => { + return from( + context.scheduleBuilder('@nrwl/jest:jest', { + tsConfig: options.tsSpecConfig, + jestConfig: options.jestConfig, + watch: false + }) + ).pipe(concatMap(run => run.output)); + }) + ); +} + +function buildTarget(context: BuilderContext, target: string) { + return scheduleTargetAndForget(context, targetFromTargetString(target)); +} diff --git a/packages/nx-plugin/src/builders/e2e/schema.d.ts b/packages/nx-plugin/src/builders/e2e/schema.d.ts new file mode 100644 index 0000000000..935996327e --- /dev/null +++ b/packages/nx-plugin/src/builders/e2e/schema.d.ts @@ -0,0 +1,7 @@ +import { JsonObject } from '@angular-devkit/core'; + +export interface Schema extends JsonObject { + target: string; + jestConfig: string; + tsSpecConfig: string; +} diff --git a/packages/nx-plugin/src/builders/e2e/schema.json b/packages/nx-plugin/src/builders/e2e/schema.json new file mode 100644 index 0000000000..b27e7d4dc5 --- /dev/null +++ b/packages/nx-plugin/src/builders/e2e/schema.json @@ -0,0 +1,20 @@ +{ + "title": "Nx Plugin Playground Target", + "description": "Creates a playground for a Nx Plugin", + "type": "object", + "properties": { + "target": { + "type": "string", + "description": "the target Nx Plugin project and build" + }, + "jestConfig": { + "type": "string", + "description": "Jest config file" + }, + "tsSpecConfig": { + "type": "string", + "description": "Spec tsconfig file" + } + }, + "required": ["target", "jestConfig", "tsSpecConfig"] +} diff --git a/packages/nx-plugin/src/schematics/e2e-project/e2e.spec.ts b/packages/nx-plugin/src/schematics/e2e-project/e2e.spec.ts new file mode 100644 index 0000000000..b92fcf538a --- /dev/null +++ b/packages/nx-plugin/src/schematics/e2e-project/e2e.spec.ts @@ -0,0 +1,130 @@ +import { Tree } from '@angular-devkit/schematics'; +import { createEmptyWorkspace } from '@nrwl/workspace/testing'; +import { + updateWorkspace, + readJsonInTree, + readWorkspace, + getWorkspace +} from '@nrwl/workspace'; +import { runSchematic } from '../../utils/testing'; + +describe('NxPlugin e2e-project', () => { + let appTree: Tree; + beforeEach(() => { + appTree = createEmptyWorkspace(Tree.empty()); + // add a plugin project to the workspace for validations + updateWorkspace(workspace => { + workspace.projects.add({ name: 'my-plugin', root: 'libs/my-plugin' }); + })(appTree, null); + }); + + it('should validate the plugin name', async () => { + await expect( + runSchematic( + 'e2e-project', + { + pluginName: 'my-plugin', + pluginOutputPath: `dist/libs/my-plugin`, + npmPackageName: '@proj/my-plugin' + }, + appTree + ) + ).resolves.not.toThrow(); + + await expect( + runSchematic( + 'e2e-project', + { + pluginName: 'my-nonexistentplugin', + pluginOutputPath: `dist/libs/my-nonexistentplugin`, + npmPackageName: '@proj/my-nonexistentplugin' + }, + appTree + ) + ).rejects.toThrow(); + }); + + it('should add files related to e2e', async () => { + const tree = await runSchematic( + 'e2e-project', + { + pluginName: 'my-plugin', + pluginOutputPath: `dist/libs/my-plugin`, + npmPackageName: '@proj/my-plugin' + }, + appTree + ); + expect(tree.exists('apps/my-plugin-e2e/tsconfig.json')).toBeTruthy(); + expect( + tree.exists('apps/my-plugin-e2e/tests/my-plugin.test.ts') + ).toBeTruthy(); + }); + + it('should update the nxJson', async () => { + const tree = await runSchematic( + 'e2e-project', + { + pluginName: 'my-plugin', + pluginOutputPath: `dist/libs/my-plugin`, + npmPackageName: '@proj/my-plugin' + }, + appTree + ); + expect(JSON.parse(tree.readContent('nx.json'))).toMatchObject({ + projects: { + 'my-plugin-e2e': { + tags: [], + implicitDependencies: ['my-plugin'] + } + } + }); + }); + + it('should update the workspace', async () => { + const tree = await runSchematic( + 'e2e-project', + { + pluginName: 'my-plugin', + pluginOutputPath: `dist/libs/my-plugin`, + npmPackageName: '@proj/my-plugin' + }, + appTree + ); + const workspace = await getWorkspace(tree); + const project = workspace.projects.get('my-plugin-e2e'); + expect(project).toBeTruthy(); + expect(project.root).toEqual('apps/my-plugin-e2e'); + expect(project.targets.get('e2e')).toBeTruthy(); + expect(project.targets.get('e2e')).toMatchObject({ + builder: '@nrwl/nx-plugin:e2e', + options: expect.objectContaining({ + target: 'my-plugin:build', + npmPackageName: '@proj/my-plugin', + pluginOutputPath: 'dist/libs/my-plugin' + }) + }); + }); + + it('should add jest support', async () => { + const tree = await runSchematic( + 'e2e-project', + { + pluginName: 'my-plugin', + pluginOutputPath: `dist/libs/my-plugin`, + npmPackageName: '@proj/my-plugin' + }, + appTree + ); + const workspace = await getWorkspace(tree); + const project = workspace.projects.get('my-plugin-e2e'); + expect(project.targets.get('e2e')).toMatchObject({ + options: expect.objectContaining({ + tsSpecConfig: 'apps/my-plugin-e2e/tsconfig.spec.json', + jestConfig: 'apps/my-plugin-e2e/jest.config.js' + }) + }); + + expect(tree.exists('apps/my-plugin-e2e/tsconfig.spec.json')).toBeTruthy(); + expect(tree.exists('apps/my-plugin-e2e/jest.config.js')).toBeTruthy(); + }); +}); diff --git a/packages/nx-plugin/src/schematics/e2e-project/e2e.ts b/packages/nx-plugin/src/schematics/e2e-project/e2e.ts new file mode 100644 index 0000000000..de55164003 --- /dev/null +++ b/packages/nx-plugin/src/schematics/e2e-project/e2e.ts @@ -0,0 +1,143 @@ +import { normalize } from '@angular-devkit/core'; +import { WorkspaceDefinition } from '@angular-devkit/core/src/workspace'; +import { + apply, + chain, + externalSchematic, + mergeWith, + move, + Rule, + SchematicContext, + SchematicsException, + template, + Tree, + url +} from '@angular-devkit/schematics'; +import { + addProjectToNxJsonInTree, + getWorkspace, + offsetFromRoot, + readNxJsonInTree, + toPropertyName, + updateWorkspace +} from '@nrwl/workspace'; +import { join } from 'path'; +import { Schema } from './schema'; + +export interface NxPluginE2ESchema extends Schema { + projectRoot: string; + projectName: string; + pluginPropertyName: string; + npmScope: string; +} + +export default function(options: Schema): Rule { + return async (host: Tree, context: SchematicContext) => { + const workspace = await getWorkspace(host); + validatePlugin(workspace, options.pluginName); + const normalizedOptions = normalizeOptions(host, options); + return chain([ + updateFiles(normalizedOptions), + updateNxJson(normalizedOptions), + updateWorkspaceJson(normalizedOptions), + addJest(normalizedOptions) + ]); + }; +} + +function validatePlugin(workspace: WorkspaceDefinition, pluginName: string) { + const project = workspace.projects.get(pluginName); + if (!project) { + throw new SchematicsException( + `Project name "${pluginName}" doesn't not exist.` + ); + } +} + +function normalizeOptions(host: Tree, options: Schema): NxPluginE2ESchema { + const projectName = `${options.pluginName}-e2e`; + const projectRoot = join(normalize('apps'), projectName); + const npmScope = readNxJsonInTree(host).npmScope; + const pluginPropertyName = toPropertyName(options.pluginName); + return { + ...options, + projectName, + pluginPropertyName, + projectRoot, + npmScope + }; +} + +function updateNxJson(options: NxPluginE2ESchema): Rule { + return addProjectToNxJsonInTree(options.projectName, { + tags: [], + implicitDependencies: [options.pluginName] + }); +} + +function updateWorkspaceJson(options: NxPluginE2ESchema): Rule { + return chain([ + async (host, context) => { + const workspace = await getWorkspace(host); + workspace.projects.add({ + name: options.projectName, + root: options.projectRoot, + projectType: 'application', + sourceRoot: `${options.projectRoot}/src`, + targets: { + e2e: { + builder: '@nrwl/nx-plugin:e2e', + options: { + target: `${options.pluginName}:build`, + npmPackageName: options.npmPackageName, + pluginOutputPath: options.pluginOutputPath + } + } + } + }); + return updateWorkspace(workspace); + } + ]); +} + +function updateFiles(options: NxPluginE2ESchema): Rule { + return mergeWith( + apply(url('./files'), [ + template({ + tmpl: '', + ...options, + offsetFromRoot: offsetFromRoot(options.projectRoot) + }), + move(options.projectRoot) + ]) + ); +} + +function addJest(options: NxPluginE2ESchema): Rule { + return chain([ + externalSchematic('@nrwl/jest', 'jest-project', { + project: options.projectName, + setupFile: 'none', + supportTsx: false, + skipSerializers: true + }), + async (host, context) => { + const workspace = await getWorkspace(host); + const project = workspace.projects.get(options.projectName); + const testOptions = project.targets.get('test').options; + const e2eOptions = project.targets.get('e2e').options; + project.targets.get('e2e').options = { + ...e2eOptions, + ...{ + jestConfig: testOptions.jestConfig, + tsSpecConfig: testOptions.tsConfig + } + }; + + // remove the jest build target + project.targets.delete('test'); + + return updateWorkspace(workspace); + } + ]); +} diff --git a/packages/nx-plugin/src/schematics/e2e-project/files/tests/__pluginName__.test.ts__tmpl__ b/packages/nx-plugin/src/schematics/e2e-project/files/tests/__pluginName__.test.ts__tmpl__ new file mode 100644 index 0000000000..11bc9c8aba --- /dev/null +++ b/packages/nx-plugin/src/schematics/e2e-project/files/tests/__pluginName__.test.ts__tmpl__ @@ -0,0 +1,44 @@ +import { + checkFilesExist, + ensureNxProject, + readJson, + runNxCommandAsync, + uniq, +} from '@nrwl/nx-plugin/testing'; +describe('<%= pluginName %> e2e', () => { + it('should create <%= pluginName %>', async (done) => { + const plugin = uniq('<%= pluginName %>'); + ensureNxProject('<%= npmPackageName %>', '<%= pluginOutputPath %>'); + await runNxCommandAsync(`generate <%=npmPackageName%>:<%= pluginPropertyName %> ${plugin}`); + + const result = await runNxCommandAsync(`build ${plugin}`); + expect(result.stdout).toContain('Builder ran'); + + done(); + }) + + describe('--directory', () => { + it('should create src in the specified directory', async (done) => { + const plugin = uniq('<%= pluginName %>'); + ensureNxProject('<%= npmPackageName %>', '<%= pluginOutputPath %>'); + await runNxCommandAsync( + `generate <%=npmPackageName%>:<%= pluginPropertyName %> ${plugin} --directory subdir` + ); + expect(() => checkFilesExist(`libs/subdir/${plugin}/src/index.ts`)).not.toThrow(); + done(); + }); + }); + + describe('--tags', () => { + it('should add tags to nx.json', async (done) => { + const plugin = uniq('<%= pluginName %>'); + ensureNxProject('<%= npmPackageName %>', '<%= pluginOutputPath %>'); + await runNxCommandAsync( + `generate <%=npmPackageName%>:<%= pluginPropertyName %> ${plugin} --tags e2etag,e2ePackage` + ); + const nxJson = readJson('nx.json'); + expect(nxJson.projects[plugin].tags).toEqual(['e2etag', 'e2ePackage']); + done(); + }); + }); +}) \ No newline at end of file diff --git a/packages/nx-plugin/src/schematics/e2e-project/files/tsconfig.json__tmpl__ b/packages/nx-plugin/src/schematics/e2e-project/files/tsconfig.json__tmpl__ new file mode 100644 index 0000000000..ca8fd1d35b --- /dev/null +++ b/packages/nx-plugin/src/schematics/e2e-project/files/tsconfig.json__tmpl__ @@ -0,0 +1,9 @@ +{ + "extends": "<%= offsetFromRoot %>tsconfig.json", + "compilerOptions": { + "types": [ + "node" + ] + }, + "include": ["**/*.ts"] +} diff --git a/packages/nx-plugin/src/schematics/e2e-project/schema.d.ts b/packages/nx-plugin/src/schematics/e2e-project/schema.d.ts new file mode 100644 index 0000000000..cc890018d6 --- /dev/null +++ b/packages/nx-plugin/src/schematics/e2e-project/schema.d.ts @@ -0,0 +1,9 @@ +import { Linter } from '@nrwl/workspace'; + +export interface Schema { + pluginName: string; + npmPackageName: string; + pluginOutputPath: string; + jestConfig: string; + tsSpecConfig: string; +} diff --git a/packages/nx-plugin/src/schematics/e2e-project/schema.json b/packages/nx-plugin/src/schematics/e2e-project/schema.json new file mode 100644 index 0000000000..6716630a87 --- /dev/null +++ b/packages/nx-plugin/src/schematics/e2e-project/schema.json @@ -0,0 +1,28 @@ +{ + "id": "NxPluginE2E", + "title": "Create an e2e app for a Nx Plugin", + "type": "object", + "properties": { + "pluginName": { + "type": "string", + "description": "the name of the pluging to be tested" + }, + "npmPackageName": { + "type": "string", + "description": "the name of the package that would be published to NPM" + }, + "pluginOutputPath": { + "type": "string", + "description": "the output path of the plugin after it builds" + }, + "jestConfig": { + "type": "string", + "description": "Jest config file" + }, + "tsSpecConfig": { + "type": "string", + "description": "Spec tsconfig file" + } + }, + "required": ["pluginName", "npmPackageName"] +} diff --git a/packages/nx-plugin/src/schematics/plugin/files/plugin/builders.json__tmpl__ b/packages/nx-plugin/src/schematics/plugin/files/plugin/builders.json__tmpl__ new file mode 100644 index 0000000000..ca21d29553 --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/files/plugin/builders.json__tmpl__ @@ -0,0 +1,10 @@ +{ + "$schema": "<%= offsetFromRoot %>node_modules/@angular-devkit/architect/src/builders-schema.json", + "builders": { + "build": { + "implementation": "./src/builders/<%= fileName %>/builder", + "schema": "./src/builders/<%= fileName %>/schema.json", + "description": "<%= name %> builder" + } + } +} diff --git a/packages/nx-plugin/src/schematics/plugin/files/plugin/collection.json__tmpl__ b/packages/nx-plugin/src/schematics/plugin/files/plugin/collection.json__tmpl__ new file mode 100644 index 0000000000..6813ad39fe --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/files/plugin/collection.json__tmpl__ @@ -0,0 +1,12 @@ +{ + "$schema": "<%= offsetFromRoot %>node_modules/@angular-devkit/schematics/collection-schema.json", + "name": "<%= name %>", + "version": "0.0.1", + "schematics": { + "<%= propertyName %>": { + "factory": "./src/schematics/<%= fileName %>/schematic", + "schema": "./src/schematics/<%= fileName %>/schema.json", + "description": "<%= name %> schematic" + } + } +} \ No newline at end of file diff --git a/packages/nx-plugin/src/schematics/plugin/files/plugin/package.json__tmpl__ b/packages/nx-plugin/src/schematics/plugin/files/plugin/package.json__tmpl__ new file mode 100644 index 0000000000..bd50fddfab --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/files/plugin/package.json__tmpl__ @@ -0,0 +1,7 @@ +{ + "name": "<%= npmPackageName %>", + "version": "0.0.1", + "main": "src/index.js", + "schematics": "./collection.json", + "builders": "./builders.json", +} \ No newline at end of file diff --git a/packages/nx-plugin/src/schematics/plugin/files/plugin/src/builders/__fileName__/builder.spec.ts__tmpl__ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/builders/__fileName__/builder.spec.ts__tmpl__ new file mode 100644 index 0000000000..a320e4dcfc --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/builders/__fileName__/builder.spec.ts__tmpl__ @@ -0,0 +1,40 @@ +import { Architect } from '@angular-devkit/architect'; +import { TestingArchitectHost } from '@angular-devkit/architect/testing'; +import { schema } from '@angular-devkit/core'; +import { join } from 'path'; +import { <%= className %>BuilderSchema } from './schema'; + +const options: <%= className %>BuilderSchema = {}; + +describe('Command Runner Builder', () => { + let architect: Architect; + let architectHost: TestingArchitectHost; + + beforeEach(async () => { + const registry = new schema.CoreSchemaRegistry(); + registry.addPostTransform(schema.transforms.addUndefinedDefaults); + + architectHost = new TestingArchitectHost('/root', '/root'); + architect = new Architect(architectHost, registry); + + // This will either take a Node package name, or a path to the directory + // for the package.json file. + await architectHost.addBuilderFromPackage(join(__dirname, '../../..')); + }); + + it('can run', async () => { + // A "run" can have multiple outputs, and contains progress information. + const run = await architect.scheduleBuilder('@<%= npmScope %>/<%= fileName %>:build', options); // We pass the logger for checking later. + + // The "result" member (of type BuilderOutput) is the next output. + const output = await run.result; + + // Stop the builder from running. This stops Architect from keeping + // the builder-associated states in memory, since builders keep waiting + // to be scheduled. + await run.stop(); + + // Expect that it succeeded. + expect(output.success).toBe(true); + }); +}); \ No newline at end of file diff --git a/packages/nx-plugin/src/schematics/plugin/files/plugin/src/builders/__fileName__/builder.ts__tmpl__ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/builders/__fileName__/builder.ts__tmpl__ new file mode 100644 index 0000000000..10364e343f --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/builders/__fileName__/builder.ts__tmpl__ @@ -0,0 +1,19 @@ +import { + BuilderContext, + BuilderOutput, + createBuilder +} from '@angular-devkit/architect'; +import { Observable, of } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { <%= className %>BuilderSchema } from './schema'; + +export function runBuilder( + options: <%= className %>BuilderSchema, + context: BuilderContext +): Observable { + return of({ success: true }).pipe(tap(() => { + context.logger.info("Builder ran for <%= name %>"); + })); +} + +export default createBuilder(runBuilder); diff --git a/packages/nx-plugin/src/schematics/plugin/files/plugin/src/builders/__fileName__/schema.d.ts__tmpl__ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/builders/__fileName__/schema.d.ts__tmpl__ new file mode 100644 index 0000000000..44432adc33 --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/builders/__fileName__/schema.d.ts__tmpl__ @@ -0,0 +1,3 @@ +import { JsonObject } from '@angular-devkit/core'; + +export interface <%= className %>BuilderSchema extends JsonObject {} diff --git a/packages/nx-plugin/src/schematics/plugin/files/plugin/src/builders/__fileName__/schema.json__tmpl__ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/builders/__fileName__/schema.json__tmpl__ new file mode 100644 index 0000000000..2782505ea1 --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/builders/__fileName__/schema.json__tmpl__ @@ -0,0 +1,9 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "$id": "https://json-schema.org/draft-07/schema", + "title": "<%= className %> builder", + "description": "", + "type": "object", + "properties": {}, + "required": [] +} diff --git a/packages/nx-plugin/src/schematics/plugin/files/plugin/src/index.ts__tmpl__ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/index.ts__tmpl__ new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/files/src/index.ts.template b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/files/src/index.ts.template new file mode 100644 index 0000000000..7dc0b8ee64 --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/files/src/index.ts.template @@ -0,0 +1 @@ +<%= fileTemplate %> \ No newline at end of file diff --git a/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/schema.d.ts__tmpl__ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/schema.d.ts__tmpl__ new file mode 100644 index 0000000000..bc872b99a9 --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/schema.d.ts__tmpl__ @@ -0,0 +1,5 @@ +export interface <%= className %>SchematicSchema { + name: string; + tags?: string; + directory?: string; +} \ No newline at end of file diff --git a/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/schema.json__tmpl__ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/schema.json__tmpl__ new file mode 100644 index 0000000000..f2cab296cf --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/schema.json__tmpl__ @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "id": "<%= className %>", + "title": "", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use?" + }, + "tags": { + "type": "string", + "description": "Add tags to the project (used for linting)", + "alias": "t" + }, + "directory": { + "type": "string", + "description": "A directory where the project is placed", + "alias": "d" + } + }, + "required": ["name"] +} diff --git a/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/schematic.spec.ts__tmpl__ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/schematic.spec.ts__tmpl__ new file mode 100644 index 0000000000..7129f6cbdb --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/schematic.spec.ts__tmpl__ @@ -0,0 +1,29 @@ +import { Tree } from '@angular-devkit/schematics'; +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { createEmptyWorkspace } from '@nrwl/workspace/testing'; +import { join } from 'path' + +import { <%= className %>SchematicSchema } from './schema'; + +describe('<%= name %> schematic', () => { + let appTree: Tree; + const options: <%= className %>SchematicSchema = { name: 'test' }; + + const testRunner = new SchematicTestRunner( + '@<%= npmScope %>/<%= name %>', + join(__dirname, '../../../collection.json') + ); + + beforeEach(() => { + appTree = createEmptyWorkspace(Tree.empty()); + }); + + it('should run successfully', async () => { + await expect(testRunner.runSchematicAsync( + '<%= propertyName %>', + options, + appTree + ).toPromise() + ).resolves.not.toThrowError(); + }) +}); diff --git a/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/schematic.ts__tmpl__ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/schematic.ts__tmpl__ new file mode 100644 index 0000000000..e91812c1dc --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/files/plugin/src/schematics/__fileName__/schematic.ts__tmpl__ @@ -0,0 +1,85 @@ +import { + apply, + applyTemplates, + chain, + mergeWith, + move, + Rule, + url +} from '@angular-devkit/schematics'; +import { + addProjectToNxJsonInTree, + names, + offsetFromRoot, + projectRootDir, + ProjectType, + toFileName, + updateWorkspace +} from '@nrwl/workspace'; +import { <%= className %>SchematicSchema } from './schema'; + +/** + * Depending on your needs, you can change this to either `Library` or `Application` + */ +const projectType = ProjectType.Library + +interface NormalizedSchema extends <%= className %>SchematicSchema { + projectName: string; + projectRoot: string; + projectDirectory: string; + parsedTags: string[] +} + +function normalizeOptions(options: <%= className %>SchematicSchema): NormalizedSchema { + const name = toFileName(options.name); + const projectDirectory = options.directory + ? `${toFileName(options.directory)}/${name}` + : name; + const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-'); + const projectRoot = `${projectRootDir(projectType)}/${projectDirectory}`; + const parsedTags = options.tags + ? options.tags.split(',').map(s => s.trim()) + : []; + + return { + ...options, + projectName, + projectRoot, + projectDirectory, + parsedTags + }; +} + +function addFiles(options: NormalizedSchema): Rule { + return mergeWith( + apply(url(`./files`), [ + applyTemplates({ + ...options, + ...names(options.name), + offsetFromRoot: offsetFromRoot(options.projectRoot) + }), + move(options.projectRoot) + ]) + ) +} + +export default function(options: <%= className %>SchematicSchema): Rule { + const normalizedOptions = normalizeOptions(options); + return chain([ + updateWorkspace(workspace => { + workspace.projects.add({ + name: normalizedOptions.projectName, + root: normalizedOptions.projectRoot, + sourceRoot: `${normalizedOptions.projectRoot}/src`, + projectType + }).targets.add({ + name: 'build', + builder: '<%= npmPackageName %>:build' + }) + }), + addProjectToNxJsonInTree(normalizedOptions.projectName, { tags: normalizedOptions.parsedTags }), + addFiles(normalizedOptions) + ]); +} + + diff --git a/packages/nx-plugin/src/schematics/plugin/plugin.spec.ts b/packages/nx-plugin/src/schematics/plugin/plugin.spec.ts new file mode 100644 index 0000000000..97df098624 --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/plugin.spec.ts @@ -0,0 +1,139 @@ +import * as ngSchematics from '@angular-devkit/schematics'; +import { readJsonInTree, readWorkspace } from '@nrwl/workspace'; +import { createEmptyWorkspace } from '@nrwl/workspace/testing'; +import { runSchematic } from '../../utils/testing'; + +describe('NxPlugin plugin', () => { + let appTree: ngSchematics.Tree; + beforeEach(() => { + appTree = createEmptyWorkspace(ngSchematics.Tree.empty()); + }); + + it('should update the workspace.json file', async () => { + const tree = await runSchematic('plugin', { name: 'myPlugin' }, appTree); + const workspace = await readWorkspace(tree); + const project = workspace.projects['my-plugin']; + expect(project.root).toEqual('libs/my-plugin'); + expect(project.architect.build).toEqual({ + builder: '@nrwl/node:package', + options: { + outputPath: 'dist/libs/my-plugin', + tsConfig: 'libs/my-plugin/tsconfig.lib.json', + packageJson: 'libs/my-plugin/package.json', + main: 'libs/my-plugin/src/index.ts', + assets: [ + 'libs/my-plugin/*.md', + { + input: './libs/my-plugin/src', + glob: '**/*.!(ts)', + output: './src' + }, + { + input: './libs/my-plugin', + glob: 'collection.json', + output: '.' + }, + { + input: './libs/my-plugin', + glob: 'builders.json', + output: '.' + } + ] + } + }); + expect(project.architect.lint).toEqual({ + builder: '@angular-devkit/build-angular:tslint', + options: { + exclude: ['**/node_modules/**', '!libs/my-plugin/**'], + tsConfig: [ + 'libs/my-plugin/tsconfig.lib.json', + 'libs/my-plugin/tsconfig.spec.json' + ] + } + }); + expect(project.architect.test).toEqual({ + builder: '@nrwl/jest:jest', + options: { + jestConfig: 'libs/my-plugin/jest.config.js', + tsConfig: 'libs/my-plugin/tsconfig.spec.json' + } + }); + }); + + it('should update the tsconfig.lib.json file', async () => { + const tree = await runSchematic('plugin', { name: 'myPlugin' }, appTree); + const tsLibConfig = readJsonInTree( + tree, + 'libs/my-plugin/tsconfig.lib.json' + ); + expect(tsLibConfig.compilerOptions.rootDir).toEqual('.'); + }); + + it('should create schematic and builder files', async () => { + const tree = await runSchematic('plugin', { name: 'myPlugin' }, appTree); + expect(tree.exists('libs/my-plugin/collection.json')).toBeTruthy(); + expect(tree.exists('libs/my-plugin/builders.json')).toBeTruthy(); + expect( + tree.exists('libs/my-plugin/src/schematics/my-plugin/schema.d.ts') + ).toBeTruthy(); + expect( + tree.exists('libs/my-plugin/src/schematics/my-plugin/schematic.ts') + ).toBeTruthy(); + expect( + tree.exists('libs/my-plugin/src/schematics/my-plugin/schematic.spec.ts') + ).toBeTruthy(); + expect( + tree.exists('libs/my-plugin/src/schematics/my-plugin/schema.json') + ).toBeTruthy(); + expect( + tree.exists('libs/my-plugin/src/schematics/my-plugin/schema.d.ts') + ).toBeTruthy(); + expect( + tree.exists( + 'libs/my-plugin/src/schematics/my-plugin/files/src/index.ts.template' + ) + ).toBeTruthy(); + expect( + tree.readContent( + 'libs/my-plugin/src/schematics/my-plugin/files/src/index.ts.template' + ) + ).toContain('const variable = "<%= projectName %>";'); + expect( + tree.exists('libs/my-plugin/src/builders/my-plugin/builder.ts') + ).toBeTruthy(); + expect( + tree.exists('libs/my-plugin/src/builders/my-plugin/builder.spec.ts') + ).toBeTruthy(); + expect( + tree.exists('libs/my-plugin/src/builders/my-plugin/schema.json') + ).toBeTruthy(); + expect( + tree.exists('libs/my-plugin/src/builders/my-plugin/schema.d.ts') + ).toBeTruthy(); + }); + + it('should call the @nrwl/node:lib schematic', async () => { + const externalSchematicSpy = jest.spyOn(ngSchematics, 'externalSchematic'); + await runSchematic('plugin', { name: 'myPlugin' }, appTree); + expect(externalSchematicSpy).toBeCalledWith( + '@nrwl/node', + 'lib', + expect.objectContaining({ + publishable: true + }) + ); + }); + + it('should call the @nrwl/nx-plugin:e2e schematic', async () => { + const schematicSpy = jest.spyOn(ngSchematics, 'schematic'); + const tree = await runSchematic('plugin', { name: 'myPlugin' }, appTree); + expect(schematicSpy).toBeCalledWith( + 'e2e-project', + expect.objectContaining({ + pluginName: 'my-plugin', + pluginOutputPath: `dist/libs/my-plugin`, + npmPackageName: '@proj/my-plugin' + }) + ); + }); +}); diff --git a/packages/nx-plugin/src/schematics/plugin/plugin.ts b/packages/nx-plugin/src/schematics/plugin/plugin.ts new file mode 100644 index 0000000000..65eb7b5bf9 --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/plugin.ts @@ -0,0 +1,165 @@ +import { JsonArray, normalize, Path } from '@angular-devkit/core'; +import { stripIndents } from '@angular-devkit/core/src/utils/literals'; +import { + apply, + chain, + externalSchematic, + MergeStrategy, + mergeWith, + move, + Rule, + schematic, + SchematicContext, + template, + Tree, + url +} from '@angular-devkit/schematics'; +import { + formatFiles, + getProjectConfig, + names, + offsetFromRoot, + readNxJsonInTree, + toFileName, + updateJsonInTree, + updateWorkspace +} from '@nrwl/workspace'; +import { allFilesInDirInHost } from '@nrwl/workspace/src/utils/ast-utils'; +import { Schema } from './schema'; +export interface NormalizedSchema extends Schema { + name: string; + fileName: string; + projectRoot: Path; + projectDirectory: string; + parsedTags: string[]; + npmScope: string; + npmPackageName: string; + fileTemplate: string; +} + +export default function(schema: NormalizedSchema): Rule { + return (host: Tree, context: SchematicContext) => { + const options = normalizeOptions(host, schema); + + return chain([ + externalSchematic('@nrwl/node', 'lib', { + ...schema, + publishable: true + }), + addFiles(options), + updateWorkspaceJson(options), + updateTsConfig(options), + schematic('e2e-project', { + pluginName: options.name, + pluginOutputPath: `dist/libs/${options.projectDirectory}`, + npmPackageName: options.npmPackageName + }), + formatFiles(options) + ]); + }; +} + +function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { + const nxJson = readNxJsonInTree(host); + const npmScope = nxJson.npmScope; + const name = toFileName(options.name); + const projectDirectory = options.directory + ? `${toFileName(options.directory)}/${name}` + : name; + + const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-'); + const fileName = projectName; + const projectRoot = normalize(`libs/${projectDirectory}`); + + const parsedTags = options.tags + ? options.tags.split(',').map(s => s.trim()) + : []; + const npmPackageName = `@${npmScope}/${name}`; + + const fileTemplate = getFileTemplate(); + + const normalized: NormalizedSchema = { + ...options, + fileName, + npmScope, + name: projectName, + projectRoot, + projectDirectory, + parsedTags, + npmPackageName, + fileTemplate + }; + + return normalized; +} + +function addFiles(options: NormalizedSchema): Rule { + return chain([ + host => { + allFilesInDirInHost( + host, + normalize(`${options.projectRoot}/src/lib`) + ).forEach(file => { + host.delete(file); + }); + + return host; + }, + mergeWith( + apply(url(`./files/plugin`), [ + template({ + ...options, + ...names(options.name), + tmpl: '', + offsetFromRoot: offsetFromRoot(options.projectRoot) + }), + move(options.projectRoot) + ]), + MergeStrategy.Overwrite + ) + ]); +} + +function updateWorkspaceJson(options: NormalizedSchema): Rule { + return updateWorkspace(workspace => { + const targets = workspace.projects.get(options.name).targets; + const build = targets.get('build'); + if (build) { + (build.options.assets as JsonArray).push( + ...[ + { + input: `./${options.projectRoot}/src`, + glob: '**/*.!(ts)', + output: './src' + }, + { + input: `./${options.projectRoot}`, + glob: 'collection.json', + output: '.' + }, + { + input: `./${options.projectRoot}`, + glob: 'builders.json', + output: '.' + } + ] + ); + } + }); +} + +function updateTsConfig(options: NormalizedSchema): Rule { + return (host: Tree, context: SchematicContext) => { + const projectConfig = getProjectConfig(host, options.name); + return updateJsonInTree(`${projectConfig.root}/tsconfig.lib.json`, json => { + json.compilerOptions.rootDir = '.'; + return json; + }); + }; +} + +function getFileTemplate() { + return stripIndents` + const variable = "<%= projectName %>"; + `; +} diff --git a/packages/nx-plugin/src/schematics/plugin/schema.d.ts b/packages/nx-plugin/src/schematics/plugin/schema.d.ts new file mode 100644 index 0000000000..34b5b7d341 --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/schema.d.ts @@ -0,0 +1,11 @@ +import { Linter } from '@nrwl/workspace'; + +export interface Schema { + name: string; + directory?: string; + skipTsConfig: boolean; + skipFormat: boolean; + tags?: string; + unitTestRunner: 'jest' | 'none'; + linter: Linter; +} diff --git a/packages/nx-plugin/src/schematics/plugin/schema.json b/packages/nx-plugin/src/schematics/plugin/schema.json new file mode 100644 index 0000000000..28e7d0c238 --- /dev/null +++ b/packages/nx-plugin/src/schematics/plugin/schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "NxPluginPlugin", + "title": "Create a Plugin for Nx", + "type": "object", + "examples": [ + { + "command": "g plugin my-plugin --directory=plugins", + "description": "Generate libs/plugins/my-plugin" + } + ], + "properties": { + "name": { + "type": "string", + "description": "Plugin name", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use for the plugin?" + }, + "directory": { + "type": "string", + "description": "A directory where the plugin is placed", + "alias": "d" + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint", "tslint"], + "default": "tslint" + }, + "unitTestRunner": { + "type": "string", + "enum": ["jest", "none"], + "description": "Test runner to use for unit tests", + "default": "jest" + }, + "tags": { + "type": "string", + "description": "Add tags to the library (used for linting)", + "alias": "t" + }, + "skipFormat": { + "description": "Skip formatting files", + "type": "boolean", + "default": false + }, + "skipTsConfig": { + "type": "boolean", + "default": false, + "description": "Do not update tsconfig.json for development experience." + } + }, + "required": ["name"] +} diff --git a/packages/nx-plugin/src/utils/testing-utils/async-commands.ts b/packages/nx-plugin/src/utils/testing-utils/async-commands.ts new file mode 100644 index 0000000000..cb9b8f555f --- /dev/null +++ b/packages/nx-plugin/src/utils/testing-utils/async-commands.ts @@ -0,0 +1,46 @@ +import { exec } from 'child_process'; +import { tmpProjPath } from './paths'; + +/** + * Run a command asynchronously + * @param command + * @param opts + */ +export function runCommandAsync( + command: string, + opts = { + silenceError: false + } +): Promise<{ stdout: string; stderr: string }> { + return new Promise((resolve, reject) => { + exec( + command, + { + cwd: tmpProjPath() + }, + (err, stdout, stderr) => { + if (!opts.silenceError && err) { + reject(err); + } + resolve({ stdout, stderr }); + } + ); + }); +} + +/** + * Run a nx command asynchronously + * @param command + * @param opts + */ +export function runNxCommandAsync( + command: string, + opts = { + silenceError: false + } +): Promise<{ stdout: string; stderr: string }> { + return runCommandAsync( + `node ./node_modules/@nrwl/cli/bin/nx.js ${command}`, + opts + ); +} diff --git a/packages/nx-plugin/src/utils/testing-utils/commands.ts b/packages/nx-plugin/src/utils/testing-utils/commands.ts new file mode 100644 index 0000000000..fcdc752c1d --- /dev/null +++ b/packages/nx-plugin/src/utils/testing-utils/commands.ts @@ -0,0 +1,43 @@ +import { execSync } from 'child_process'; +import { tmpProjPath } from './paths'; + +/** + * Run a nx command + * @param command + * @param opts + */ +export function runNxCommand( + command?: string, + opts = { + silenceError: false + } +): string { + try { + return execSync(`node ./node_modules/@nrwl/cli/bin/nx.js ${command}`, { + cwd: tmpProjPath() + }) + .toString() + .replace( + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, + '' + ); + } catch (e) { + if (opts.silenceError) { + return e.stdout.toString(); + } else { + console.log(e.stdout.toString(), e.stderr.toString()); + throw e; + } + } +} + +export function runCommand(command: string): string { + try { + return execSync(command, { + cwd: tmpProjPath(), + stdio: ['pipe', 'pipe', 'pipe'] + }).toString(); + } catch (e) { + return e.stdout.toString() + e.stderr.toString(); + } +} diff --git a/packages/nx-plugin/src/utils/testing-utils/index.ts b/packages/nx-plugin/src/utils/testing-utils/index.ts new file mode 100644 index 0000000000..81414ad685 --- /dev/null +++ b/packages/nx-plugin/src/utils/testing-utils/index.ts @@ -0,0 +1,5 @@ +export * from './async-commands'; +export * from './commands'; +export * from './paths'; +export * from './nx-project'; +export * from './utils'; diff --git a/packages/nx-plugin/src/utils/testing-utils/nx-project.ts b/packages/nx-plugin/src/utils/testing-utils/nx-project.ts new file mode 100644 index 0000000000..18918212a6 --- /dev/null +++ b/packages/nx-plugin/src/utils/testing-utils/nx-project.ts @@ -0,0 +1,71 @@ +import { appRootPath } from '@nrwl/workspace/src/utils/app-root'; +import { execSync } from 'child_process'; +import { readFileSync, writeFileSync } from 'fs'; +import { ensureDirSync } from 'fs-extra'; +import { tmpProjPath } from './paths'; +import { cleanup, copyNodeModules } from './utils'; + +function runNxNewCommand(args?: string, silent?: boolean) { + const localTmpDir = `./tmp/nx-e2e`; + return execSync( + `node ${require.resolve( + '@nrwl/tao' + )} new proj --no-interactive --skip-install --collection=@nrwl/workspace --npmScope=proj ${args || + ''}`, + { + cwd: localTmpDir, + ...(silent && false ? { stdio: ['ignore', 'ignore', 'ignore'] } : {}) + } + ); +} + +function patchPackageJsonForPlugin(npmPackageName: string, distPath: string) { + const p = JSON.parse(readFileSync(tmpProjPath('package.json')).toString()); + p.devDependencies[npmPackageName] = `file:${appRootPath}/${distPath}`; + writeFileSync(tmpProjPath('package.json'), JSON.stringify(p, null, 2)); +} + +/** + * Generate a unique name for running CLI commands + * @param prefix + */ +export function uniq(prefix: string) { + return `${prefix}${Math.floor(Math.random() * 10000000)}`; +} + +export function runYarnInstall(silent: boolean = true) { + const install = execSync('yarn install', { + cwd: tmpProjPath(), + ...(silent ? { stdio: ['ignore', 'ignore', 'ignore'] } : {}) + }); + return install ? install.toString() : ''; +} + +/** + * Sets up a new project in the temporary project path + * for the currently selected CLI. + */ +export function newNxProject( + npmPackageName: string, + pluginDistPath: string +): void { + cleanup(); + runNxNewCommand('', true); + patchPackageJsonForPlugin(npmPackageName, pluginDistPath); + runYarnInstall(); +} + +/** + * Ensures that a project has been setup + * in the temporary project path + * + * If one is not found, it creates a new project. + */ +export function ensureNxProject( + npmPackageName?: string, + pluginDistPath?: string +): void { + ensureDirSync(tmpProjPath()); + newNxProject(npmPackageName, pluginDistPath); + copyNodeModules(['@nrwl']); +} diff --git a/packages/nx-plugin/src/utils/testing-utils/paths.ts b/packages/nx-plugin/src/utils/testing-utils/paths.ts new file mode 100644 index 0000000000..4ff3953bc6 --- /dev/null +++ b/packages/nx-plugin/src/utils/testing-utils/paths.ts @@ -0,0 +1,11 @@ +export function tmpProjPath(path?: string) { + return path + ? `${process.cwd()}/tmp/nx-e2e/proj/${path}` + : `${process.cwd()}/tmp/nx-e2e/proj`; +} + +export function tmpBackupProjPath(path?: string) { + return path + ? `${process.cwd()}/tmp/nx-e2e/proj-backup/${path}` + : `${process.cwd()}/tmp/nx-e2e/proj-backup`; +} diff --git a/packages/nx-plugin/src/utils/testing-utils/utils.ts b/packages/nx-plugin/src/utils/testing-utils/utils.ts new file mode 100644 index 0000000000..74488f7390 --- /dev/null +++ b/packages/nx-plugin/src/utils/testing-utils/utils.ts @@ -0,0 +1,110 @@ +import { + ensureDirSync, + readdirSync, + readFileSync, + removeSync, + renameSync, + statSync, + writeFileSync, + copySync +} from 'fs-extra'; +import { dirname } from 'path'; +import { tmpProjPath } from './paths'; + +/** + * Copies module folders from the working directory to the e2e directory + * @param modules a list of module names or scopes to copy + */ +export function copyNodeModules(modules: string[]) { + modules.forEach(module => { + removeSync(`${tmpProjPath()}/node_modules/${module}`); + copySync( + `./node_modules/${module}`, + `${tmpProjPath()}/node_modules/${module}` + ); + }); +} + +/** + * Assert output from a asynchronous CLI command + * @param output: Output from an asynchronous command + */ +export function expectTestsPass(v: { stdout: string; stderr: string }) { + expect(v.stderr).toContain('Ran all test suites'); + expect(v.stderr).not.toContain('fail'); +} + +export function updateFile(f: string, content: string | Function): void { + ensureDirSync(dirname(tmpProjPath(f))); + if (typeof content === 'string') { + writeFileSync(tmpProjPath(f), content); + } else { + writeFileSync( + tmpProjPath(f), + content(readFileSync(tmpProjPath(f)).toString()) + ); + } +} + +export function renameFile(f: string, newPath: string): void { + ensureDirSync(dirname(tmpProjPath(newPath))); + renameSync(tmpProjPath(f), tmpProjPath(newPath)); +} + +export function checkFilesExist(...expectedFiles: string[]) { + expectedFiles.forEach(f => { + const ff = f.startsWith('/') ? f : tmpProjPath(f); + if (!exists(ff)) { + throw new Error(`File '${ff}' does not exist`); + } + }); +} + +export function listFiles(dirName: string) { + return readdirSync(tmpProjPath(dirName)); +} + +export function readJson(f: string): any { + return JSON.parse(readFile(f)); +} + +export function readFile(f: string) { + const ff = f.startsWith('/') ? f : tmpProjPath(f); + return readFileSync(ff).toString(); +} + +export function cleanup() { + removeSync(tmpProjPath()); +} + +export function rmDist() { + removeSync(`${tmpProjPath()}/dist`); +} + +export function getCwd(): string { + return process.cwd(); +} + +export function directoryExists(filePath: string): boolean { + try { + return statSync(filePath).isDirectory(); + } catch (err) { + return false; + } +} + +export function fileExists(filePath: string): boolean { + try { + return statSync(filePath).isFile(); + } catch (err) { + return false; + } +} + +export function exists(filePath: string): boolean { + return directoryExists(filePath) || fileExists(filePath); +} + +export function getSize(filePath: string): number { + return statSync(filePath).size; +} diff --git a/packages/nx-plugin/src/utils/testing.ts b/packages/nx-plugin/src/utils/testing.ts new file mode 100644 index 0000000000..48458151f0 --- /dev/null +++ b/packages/nx-plugin/src/utils/testing.ts @@ -0,0 +1,40 @@ +/** + * Testing file for internal schematics + */ + +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { join } from 'path'; +import { Tree } from '@angular-devkit/schematics'; +import { TestingArchitectHost } from '@angular-devkit/architect/testing'; +import { schema } from '@angular-devkit/core'; +import { Architect } from '@angular-devkit/architect'; +import { MockBuilderContext } from '@nrwl/workspace/testing'; + +const testRunner = new SchematicTestRunner( + '@nrwl/nx-plugin', + join(__dirname, '../../collection.json') +); + +export function runSchematic(schematicName: string, options: T, tree: Tree) { + return testRunner.runSchematicAsync(schematicName, options, tree).toPromise(); +} + +export async function getTestArchitect() { + const architectHost = new TestingArchitectHost('/root', '/root'); + const registry = new schema.CoreSchemaRegistry(); + registry.addPostTransform(schema.transforms.addUndefinedDefaults); + + const architect = new Architect(architectHost, registry); + + await architectHost.addBuilderFromPackage(join(__dirname, '../..')); + + return [architect, architectHost] as [Architect, TestingArchitectHost]; +} + +export async function getMockContext() { + const [architect, architectHost] = await getTestArchitect(); + + const context = new MockBuilderContext(architect, architectHost); + await context.addBuilderFromPackage(join(__dirname, '../..')); + return context; +} diff --git a/packages/nx-plugin/src/utils/versions.ts b/packages/nx-plugin/src/utils/versions.ts new file mode 100644 index 0000000000..df701d624b --- /dev/null +++ b/packages/nx-plugin/src/utils/versions.ts @@ -0,0 +1 @@ +export const nxVersion = '*'; diff --git a/packages/nx-plugin/testing.ts b/packages/nx-plugin/testing.ts new file mode 100644 index 0000000000..52c637a4d1 --- /dev/null +++ b/packages/nx-plugin/testing.ts @@ -0,0 +1 @@ +export * from './src/utils/testing-utils'; diff --git a/packages/workspace/index.ts b/packages/workspace/index.ts index 48b0a8bdde..c9c0298206 100644 --- a/packages/workspace/index.ts +++ b/packages/workspace/index.ts @@ -6,6 +6,7 @@ export { names, findModuleParent } from './src/utils/name-utils'; +export { ProjectType, projectRootDir } from './src/utils/project-type'; export { serializeJson, renameSync, @@ -42,7 +43,10 @@ export { getProjectGraphFromHost, readWorkspace, renameSyncInTree, - renameDirSyncInTree + renameDirSyncInTree, + updateNxJsonInTree, + addProjectToNxJsonInTree, + readNxJsonInTree } from './src/utils/ast-utils'; export { diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 822ed0f1c4..e2eb4c7bf7 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -40,6 +40,7 @@ "@nrwl/nest", "@nrwl/next", "@nrwl/node", + "@nrwl/nx-plugin", "@nrwl/react", "@nrwl/storybook", "@nrwl/web" diff --git a/packages/workspace/src/schematics/library/library.ts b/packages/workspace/src/schematics/library/library.ts index fb8250f691..492e868be3 100644 --- a/packages/workspace/src/schematics/library/library.ts +++ b/packages/workspace/src/schematics/library/library.ts @@ -20,6 +20,7 @@ import { toFileName, names } from '@nrwl/workspace'; import { formatFiles } from '@nrwl/workspace'; import { offsetFromRoot } from '@nrwl/workspace'; import { generateProjectLint, addLintFiles } from '../../utils/lint'; +import { addProjectToNxJsonInTree } from '../../utils/ast-utils'; export interface NormalizedSchema extends Schema { name: string; @@ -81,10 +82,7 @@ function createFiles(options: NormalizedSchema): Rule { } function updateNxJson(options: NormalizedSchema): Rule { - return updateJsonInTree('nx.json', json => { - json.projects[options.name] = { tags: options.parsedTags }; - return json; - }); + return addProjectToNxJsonInTree(options.name, { tags: options.parsedTags }); } export default function(schema: Schema): Rule { @@ -117,6 +115,8 @@ function normalizeOptions(options: Schema): NormalizedSchema { const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-'); const fileName = options.simpleModuleName ? name : projectName; + + // const projectRoot = `libs/${projectDirectory}`; const projectRoot = `libs/${projectDirectory}`; const parsedTags = options.tags diff --git a/packages/workspace/src/utils/ast-utils.ts b/packages/workspace/src/utils/ast-utils.ts index dd8b1c6707..4bc5664a33 100644 --- a/packages/workspace/src/utils/ast-utils.ts +++ b/packages/workspace/src/utils/ast-utils.ts @@ -23,7 +23,10 @@ import { } from '../core/project-graph'; import { FileData } from '../core/file-utils'; import { extname, join, normalize, Path } from '@angular-devkit/core'; -import { NxJson } from '@nrwl/workspace/src/core/shared-interfaces'; +import { + NxJson, + NxJsonProjectConfig +} from '@nrwl/workspace/src/core/shared-interfaces'; function nodesByPosition(first: ts.Node, second: ts.Node): number { return first.getStart() - second.getStart(); @@ -497,6 +500,35 @@ export function updateWorkspaceInTree( }; } +export function readNxJsonInTree(host: Tree) { + return readJsonInTree(host, 'nx.json'); +} + +export function updateNxJsonInTree( + callback: (json: NxJson, context: SchematicContext) => NxJson +): Rule { + return (host: Tree, context: SchematicContext): Tree => { + host.overwrite( + 'nx.json', + serializeJson(callback(readJsonInTree(host, 'nx.json'), context)) + ); + return host; + }; +} + +export function addProjectToNxJsonInTree( + projectName: string, + options: NxJsonProjectConfig +): Rule { + const defaultOptions = { + tags: [] + }; + return updateNxJsonInTree(json => { + json.projects[projectName] = { ...defaultOptions, ...options }; + return json; + }); +} + export function readWorkspace(host: Tree): any { const path = getWorkspacePath(host); return readJsonInTree(host, path); diff --git a/packages/workspace/src/utils/project-type.ts b/packages/workspace/src/utils/project-type.ts new file mode 100644 index 0000000000..a62c7ee024 --- /dev/null +++ b/packages/workspace/src/utils/project-type.ts @@ -0,0 +1,12 @@ +export enum ProjectType { + Application = 'application', + Library = 'library' +} + +export function projectRootDir(projectType: ProjectType) { + if (projectType == ProjectType.Application) { + return 'apps'; + } else if (projectType == ProjectType.Library) { + return 'libs'; + } +} diff --git a/scripts/build.sh b/scripts/build.sh index 9083f0f497..7bed1a350a 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -29,6 +29,7 @@ rm -rf build/packages/angular/bundles/nrwl-angular-testing.umd.min.js.bak rsync -a --exclude=*.ts packages/ build/packages chmod +x build/packages/create-nx-workspace/bin/create-nx-workspace.js +chmod +x build/packages/nx-plugin/bin/create.js chmod +x build/packages/cli/bin/nx.js chmod +x build/packages/tao/index.js @@ -58,6 +59,7 @@ cp README.md build/packages/tao cp README.md build/packages/eslint-plugin-nx cp README.md build/packages/linter cp README.md build/packages/bazel +cp README.md build/packages/nx-plugin cp LICENSE build/packages/builders cp LICENSE build/packages/schematics @@ -79,6 +81,7 @@ cp LICENSE build/packages/tao cp LICENSE build/packages/eslint-plugin-nx cp LICENSE build/packages/linter cp LICENSE build/packages/bazel +cp LICENSE build/packages/nx-plugin echo "Nx libraries available at build/packages:" ls build/packages diff --git a/scripts/commit-lint.js b/scripts/commit-lint.js index 4f24e0031c..2b87addca1 100755 --- a/scripts/commit-lint.js +++ b/scripts/commit-lint.js @@ -6,7 +6,7 @@ const gitMessage = require('child_process') .toString() .trim(); -const matchCommit = /(chore|feat|fix|cleanup|docs)\((angular|bazel|core|docs|nextjs|node|react|storybook|testing|repo|misc)\):\s(([a-z0-9:\-\s])+)/g.test( +const matchCommit = /(chore|feat|fix|cleanup|docs)\((angular|bazel|core|docs|nextjs|node|nx-plugin|react|storybook|testing|repo|misc)\):\s(([a-z0-9:\-\s])+)/g.test( gitMessage ); const matchRevert = /Revert/gi.test(gitMessage); diff --git a/scripts/e2e-ci2.sh b/scripts/e2e-ci2.sh index 92231e322a..3eaf4078e6 100755 --- a/scripts/e2e-ci2.sh +++ b/scripts/e2e-ci2.sh @@ -12,6 +12,7 @@ export SELECTED_CLI=$1 jest --maxWorkers=1 ./build/e2e/ng-add.test.js && jest --maxWorkers=1 ./build/e2e/ngrx.test.js && jest --maxWorkers=1 ./build/e2e/node.test.js && +jest --maxWorkers=1 ./build/e2e/nx-plugin.test.js && jest --maxWorkers=1 ./build/e2e/print-affected.test.js && jest --maxWorkers=1 ./build/e2e/react.test.js && jest --maxWorkers=1 ./build/e2e/report.test.js && diff --git a/scripts/nx-release.js b/scripts/nx-release.js index 48a3446dbd..a7bbda5046 100755 --- a/scripts/nx-release.js +++ b/scripts/nx-release.js @@ -165,7 +165,8 @@ const options = { 'build/npm/cli/package.json', 'build/npm/tao/package.json', 'build/npm/eslint-plugin-nx/package.json', - 'build/npm/linter/package.json' + 'build/npm/linter/package.json', + 'build/npm/nx-plugin/package.json' ], increment: parsedVersion.version, requireUpstream: false, diff --git a/scripts/package.sh b/scripts/package.sh index 7967b6762f..ee4895414b 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -18,25 +18,33 @@ cd build/packages if [[ "$OSTYPE" == "darwin"* ]]; then sed -i "" "s|exports.nxVersion = '\*';|exports.nxVersion = '$NX_VERSION';|g" {react,next,web,jest,node,express,nest,cypress,storybook,angular,workspace}/src/utils/versions.js - sed -i "" "s|\*|$NX_VERSION|g" {schematics,react,next,web,jest,node,express,nest,cypress,storybook,angular,workspace,cli,linter,bazel,tao,eslint-plugin-nx,create-nx-workspace}/package.json + sed -i "" "s|\*|$NX_VERSION|g" {schematics,react,next,web,jest,node,express,nest,cypress,storybook,angular,workspace,cli,linter,bazel,tao,eslint-plugin-nx,create-nx-workspace,nx-plugin}/package.json sed -i "" "s|NX_VERSION|$NX_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js sed -i "" "s|ANGULAR_CLI_VERSION|$ANGULAR_CLI_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js sed -i "" "s|TYPESCRIPT_VERSION|$TYPESCRIPT_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js sed -i "" "s|PRETTIER_VERSION|$PRETTIER_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js + sed -i "" "s|NX_VERSION|$NX_VERSION|g" nx-plugin/bin/create.js + sed -i "" "s|ANGULAR_CLI_VERSION|$ANGULAR_CLI_VERSION|g" nx-plugin/bin/create.js + sed -i "" "s|TYPESCRIPT_VERSION|$TYPESCRIPT_VERSION|g" nx-plugin/bin/create.js + sed -i "" "s|PRETTIER_VERSION|$PRETTIER_VERSION|g" nx-plugin/bin/create.js else sed -i "s|exports.nxVersion = '\*';|exports.nxVersion = '$NX_VERSION';|g" {react,next,web,jest,node,express,nest,cypress,storybook,angular,workspace}/src/utils/versions.js - sed -i "s|\*|$NX_VERSION|g" {schematics,react,next,web,jest,node,express,nest,cypress,storybook,angular,workspace,cli,linter,bazel,tao,eslint-plugin-nx,create-nx-workspace}/package.json + sed -i "s|\*|$NX_VERSION|g" {schematics,react,next,web,jest,node,express,nest,cypress,storybook,angular,workspace,cli,linter,bazel,tao,eslint-plugin-nx,create-nx-workspace,nx-plugin}/package.json sed -i "s|NX_VERSION|$NX_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js sed -i "s|ANGULAR_CLI_VERSION|$ANGULAR_CLI_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js sed -i "s|TYPESCRIPT_VERSION|$TYPESCRIPT_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js sed -i "s|PRETTIER_VERSION|$PRETTIER_VERSION|g" create-nx-workspace/bin/create-nx-workspace.js + sed -i "s|NX_VERSION|$NX_VERSION|g" nx-plugin/bin/create.js + sed -i "s|ANGULAR_CLI_VERSION|$ANGULAR_CLI_VERSION|g" nx-plugin/bin/create.js + sed -i "s|TYPESCRIPT_VERSION|$TYPESCRIPT_VERSION|g" nx-plugin/bin/create.js + sed -i "s|PRETTIER_VERSION|$PRETTIER_VERSION|g" nx-plugin/bin/create.js fi if [[ $NX_VERSION == "*" ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then - sed -E -i "" "s|\"@nrwl\/([^\"]+)\": \"\\*\"|\"@nrwl\/\1\": \"file:$PWD\/\1\"|" {schematics,jest,web,react,next,node,express,nest,cypress,storybook,angular,workspace,linter,bazel,cli,tao,eslint-plugin-nx,create-nx-workspace}/package.json + sed -E -i "" "s|\"@nrwl\/([^\"]+)\": \"\\*\"|\"@nrwl\/\1\": \"file:$PWD\/\1\"|" {schematics,jest,web,react,next,node,express,nest,cypress,storybook,angular,workspace,linter,bazel,cli,tao,eslint-plugin-nx,create-nx-workspace,nx-plugin}/package.json else echo $PWD - sed -E -i "s|\"@nrwl\/([^\"]+)\": \"\\*\"|\"@nrwl\/\1\": \"file:$PWD\/\1\"|" {schematics,jest,web,react,next,node,express,nest,cypress,storybook,angular,workspace,linter,bazel,cli,tao,eslint-plugin-nx,create-nx-workspace}/package.json + sed -E -i "s|\"@nrwl\/([^\"]+)\": \"\\*\"|\"@nrwl\/\1\": \"file:$PWD\/\1\"|" {schematics,jest,web,react,next,node,express,nest,cypress,storybook,angular,workspace,linter,bazel,cli,tao,eslint-plugin-nx,create-nx-workspace,nx-plugin}/package.json fi fi diff --git a/scripts/test.sh b/scripts/test.sh index 66a6c003ac..b20758056b 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,5 +3,5 @@ if [ -n "$1" ]; then jest --maxWorkers=1 ./build/packages/$1.spec.js else - jest --maxWorkers=1 ./build/packages/{schematics,bazel,builders,react,jest,web,node,express,nest,cypress,storybook,angular,workspace,tao,eslint-plugin-nx,next} --passWithNoTests + jest --maxWorkers=1 ./build/packages/{schematics,bazel,builders,react,jest,web,node,express,nest,cypress,storybook,angular,workspace,tao,eslint-plugin-nx,next,nx-plugin} --passWithNoTests fi diff --git a/yarn.lock b/yarn.lock index 66faeb9dd8..0554459e47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4167,6 +4167,13 @@ resolved "https://registry.yarnpkg.com/@types/fast-levenshtein/-/fast-levenshtein-0.0.1.tgz#3a3615cf173645c8fca58d051e4e32824e4bd286" integrity sha1-OjYVzxc2Rcj8pY0FHk4ygk5L0oY= +"@types/fs-extra@7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-7.0.0.tgz#9c4ad9e1339e7448a76698829def1f159c1b636c" + integrity sha512-ndoMMbGyuToTy4qB6Lex/inR98nPiNHacsgMPvy+zqMLgSxbt8VtWpDArpGp69h1fEDQHn1KB+9DWD++wgbwYA== + dependencies: + "@types/node" "*" + "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"