diff --git a/.cz-config.js b/.cz-config.js index 29d32a016a..f2a0a0bb3a 100644 --- a/.cz-config.js +++ b/.cz-config.js @@ -20,6 +20,7 @@ module.exports = { { name: 'core', description: 'anything Nx core specific' }, { name: 'docs', description: 'anything related to docs infrastructure' }, { name: 'nextjs', description: 'anything Next specific' }, + { name: 'nest', description: 'anything Nest specific' }, { name: 'node', description: 'anything Node specific' }, { name: 'nx-plugin', description: 'anything Nx Plugin specific' }, { name: 'react', description: 'anything React specific' }, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 941ed7a06f..b7e29f4e6a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -181,6 +181,7 @@ The scope must be one of the following: - core - anything Nx core specific - docs - anything related to docs infrastructure - nextjs - anything Next specific +- nest - anything Nest specific - node - anything Node specific - linter - anything Linter specific - react - anything React specific diff --git a/docs/angular/api-nest/schematics/library.md b/docs/angular/api-nest/schematics/library.md new file mode 100644 index 0000000000..362448cc46 --- /dev/null +++ b/docs/angular/api-nest/schematics/library.md @@ -0,0 +1,145 @@ +# library + +Create a new nest library + +## Usage + +```bash +ng generate library ... +``` + +```bash +ng g lib ... # same +``` + +By default, Nx will search for `library` in the default collection provisioned in `angular.json`. + +You can specify the collection explicitly as follows: + +```bash +ng g @nrwl/nest:library ... +``` + +Show what will be generated without writing to disk: + +```bash +ng g library ... --dry-run +``` + +### Examples + +Generate libs/myapp/mylib: + +```bash +ng g lib mylib --directory=myapp +``` + +## Options + +### controller + +Default: `false` + +Type: `boolean` + +Include a controller with the library + +### directory + +Alias(es): d + +Type: `string` + +A directory where the app is placed + +### global + +Default: `false` + +Type: `boolean` + +Add the Global decorator to the generated module. + +### linter + +Default: `tslint` + +Type: `string` + +Possible values: `eslint`, `tslint` + +The tool to use for running lint checks. + +### name + +Type: `string` + +Library name + +### publishable + +Type: `boolean` + +Create a publishable library. A "build" architect will be added for this project the workspace configuration. + +### service + +Default: `false` + +Type: `boolean` + +Include a service with the library. + +### 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) + +### target + +Default: `es6` + +Type: `string` + +Possible values: `es5`, `es6`, `esnext`, `es2015`, `es2016`, `es2017`, `es2018`, `es2019`, `es2020` + +The es target, Nest suggest using es6 or higher. + +### testEnvironment + +Default: `node` + +Type: `string` + +Possible values: `jsdom`, `node` + +The test environment for jest, for node applications this should stay as node unless doing DOM testing. + +### unitTestRunner + +Default: `jest` + +Type: `string` + +Possible values: `jest`, `none` + +Test runner to use for unit tests diff --git a/docs/angular/api-node/schematics/library.md b/docs/angular/api-node/schematics/library.md index bff4598616..be1ca7b31a 100644 --- a/docs/angular/api-node/schematics/library.md +++ b/docs/angular/api-node/schematics/library.md @@ -90,6 +90,16 @@ Type: `string` Add tags to the library (used for linting) +### testEnvironment + +Default: `jsdom` + +Type: `string` + +Possible values: `jsdom`, `node` + +The test environment to use if unitTestRunner is set to jest + ### unitTestRunner Default: `jest` diff --git a/docs/angular/api-workspace/schematics/library.md b/docs/angular/api-workspace/schematics/library.md index e24a4e2c17..e5bf9c09fb 100644 --- a/docs/angular/api-workspace/schematics/library.md +++ b/docs/angular/api-workspace/schematics/library.md @@ -80,6 +80,16 @@ Type: `string` Add tags to the library (used for linting) +### testEnvironment + +Default: `jsdom` + +Type: `string` + +Possible values: `jsdom`, `node` + +The test environment to use if unitTestRunner is set to jest + ### unitTestRunner Default: `jest` diff --git a/docs/react/api-nest/schematics/library.md b/docs/react/api-nest/schematics/library.md new file mode 100644 index 0000000000..f0e73825c6 --- /dev/null +++ b/docs/react/api-nest/schematics/library.md @@ -0,0 +1,145 @@ +# library + +Create a new nest library + +## Usage + +```bash +nx generate library ... +``` + +```bash +nx g lib ... # same +``` + +By default, Nx will search for `library` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/nest:library ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g library ... --dry-run +``` + +### Examples + +Generate libs/myapp/mylib: + +```bash +nx g lib mylib --directory=myapp +``` + +## Options + +### controller + +Default: `false` + +Type: `boolean` + +Include a controller with the library + +### directory + +Alias(es): d + +Type: `string` + +A directory where the app is placed + +### global + +Default: `false` + +Type: `boolean` + +Add the Global decorator to the generated module. + +### linter + +Default: `tslint` + +Type: `string` + +Possible values: `eslint`, `tslint` + +The tool to use for running lint checks. + +### name + +Type: `string` + +Library name + +### publishable + +Type: `boolean` + +Create a publishable library. A "build" architect will be added for this project the workspace configuration. + +### service + +Default: `false` + +Type: `boolean` + +Include a service with the library. + +### 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) + +### target + +Default: `es6` + +Type: `string` + +Possible values: `es5`, `es6`, `esnext`, `es2015`, `es2016`, `es2017`, `es2018`, `es2019`, `es2020` + +The es target, Nest suggest using es6 or higher. + +### testEnvironment + +Default: `node` + +Type: `string` + +Possible values: `jsdom`, `node` + +The test environment for jest, for node applications this should stay as node unless doing DOM testing. + +### unitTestRunner + +Default: `jest` + +Type: `string` + +Possible values: `jest`, `none` + +Test runner to use for unit tests diff --git a/docs/react/api-node/schematics/library.md b/docs/react/api-node/schematics/library.md index 223cafcfeb..ae8ff74f2e 100644 --- a/docs/react/api-node/schematics/library.md +++ b/docs/react/api-node/schematics/library.md @@ -90,6 +90,16 @@ Type: `string` Add tags to the library (used for linting) +### testEnvironment + +Default: `jsdom` + +Type: `string` + +Possible values: `jsdom`, `node` + +The test environment to use if unitTestRunner is set to jest + ### unitTestRunner Default: `jest` diff --git a/docs/react/api-workspace/schematics/library.md b/docs/react/api-workspace/schematics/library.md index 19de246912..dba68a2e9c 100644 --- a/docs/react/api-workspace/schematics/library.md +++ b/docs/react/api-workspace/schematics/library.md @@ -80,6 +80,16 @@ Type: `string` Add tags to the library (used for linting) +### testEnvironment + +Default: `jsdom` + +Type: `string` + +Possible values: `jsdom`, `node` + +The test environment to use if unitTestRunner is set to jest + ### unitTestRunner Default: `jest` diff --git a/docs/web/api-nest/schematics/library.md b/docs/web/api-nest/schematics/library.md new file mode 100644 index 0000000000..f0e73825c6 --- /dev/null +++ b/docs/web/api-nest/schematics/library.md @@ -0,0 +1,145 @@ +# library + +Create a new nest library + +## Usage + +```bash +nx generate library ... +``` + +```bash +nx g lib ... # same +``` + +By default, Nx will search for `library` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/nest:library ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g library ... --dry-run +``` + +### Examples + +Generate libs/myapp/mylib: + +```bash +nx g lib mylib --directory=myapp +``` + +## Options + +### controller + +Default: `false` + +Type: `boolean` + +Include a controller with the library + +### directory + +Alias(es): d + +Type: `string` + +A directory where the app is placed + +### global + +Default: `false` + +Type: `boolean` + +Add the Global decorator to the generated module. + +### linter + +Default: `tslint` + +Type: `string` + +Possible values: `eslint`, `tslint` + +The tool to use for running lint checks. + +### name + +Type: `string` + +Library name + +### publishable + +Type: `boolean` + +Create a publishable library. A "build" architect will be added for this project the workspace configuration. + +### service + +Default: `false` + +Type: `boolean` + +Include a service with the library. + +### 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) + +### target + +Default: `es6` + +Type: `string` + +Possible values: `es5`, `es6`, `esnext`, `es2015`, `es2016`, `es2017`, `es2018`, `es2019`, `es2020` + +The es target, Nest suggest using es6 or higher. + +### testEnvironment + +Default: `node` + +Type: `string` + +Possible values: `jsdom`, `node` + +The test environment for jest, for node applications this should stay as node unless doing DOM testing. + +### unitTestRunner + +Default: `jest` + +Type: `string` + +Possible values: `jest`, `none` + +Test runner to use for unit tests diff --git a/docs/web/api-node/schematics/library.md b/docs/web/api-node/schematics/library.md index 223cafcfeb..ae8ff74f2e 100644 --- a/docs/web/api-node/schematics/library.md +++ b/docs/web/api-node/schematics/library.md @@ -90,6 +90,16 @@ Type: `string` Add tags to the library (used for linting) +### testEnvironment + +Default: `jsdom` + +Type: `string` + +Possible values: `jsdom`, `node` + +The test environment to use if unitTestRunner is set to jest + ### unitTestRunner Default: `jest` diff --git a/docs/web/api-workspace/schematics/library.md b/docs/web/api-workspace/schematics/library.md index 19de246912..dba68a2e9c 100644 --- a/docs/web/api-workspace/schematics/library.md +++ b/docs/web/api-workspace/schematics/library.md @@ -80,6 +80,16 @@ Type: `string` Add tags to the library (used for linting) +### testEnvironment + +Default: `jsdom` + +Type: `string` + +Possible values: `jsdom`, `node` + +The test environment to use if unitTestRunner is set to jest + ### unitTestRunner Default: `jest` diff --git a/e2e/node.test.ts b/e2e/node.test.ts index bddc5d0222..8265e6c70f 100644 --- a/e2e/node.test.ts +++ b/e2e/node.test.ts @@ -21,6 +21,8 @@ import { setMaxWorkers, newProject } from './utils'; +import { stripIndents } from '@angular-devkit/core/src/utils/literals'; +import { readFile } from './utils'; function getData(): Promise { return new Promise(resolve => { @@ -226,6 +228,73 @@ forEachCli(currentCLIName => { }); }, 120000); + describe('nest libraries', function() { + it('should be able to generate a nest library', async () => { + ensureProject(); + const nestlib = uniq('nestlib'); + + runCLI(`generate @nrwl/nest:lib ${nestlib}`); + + const jestConfigContent = readFile(`libs/${nestlib}/jest.config.js`); + + expect(stripIndents`${jestConfigContent}`).toEqual( + stripIndents`module.exports = { + name: '${nestlib}', + preset: '../../jest.config.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]sx?$': 'ts-jest' + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + coverageDirectory: '../../coverage/libs/${nestlib}' + }; + ` + ); + + const lintResults = runCLI(`lint ${nestlib}`); + expect(lintResults).toContain('All files pass linting.'); + }, 60000); + + it('should be able to generate a nest library w/ service', async () => { + ensureProject(); + const nestlib = uniq('nestlib'); + + runCLI(`generate @nrwl/nest:lib ${nestlib} --service`); + + const lintResults = runCLI(`lint ${nestlib}`); + expect(lintResults).toContain('All files pass linting.'); + + const jestResult = await runCLIAsync(`test ${nestlib}`); + expect(jestResult.stderr).toContain('Test Suites: 1 passed, 1 total'); + }, 60000); + + it('should be able to generate a nest library w/ controller', async () => { + ensureProject(); + const nestlib = uniq('nestlib'); + + runCLI(`generate @nrwl/nest:lib ${nestlib} --controller`); + + const lintResults = runCLI(`lint ${nestlib}`); + expect(lintResults).toContain('All files pass linting.'); + + const jestResult = await runCLIAsync(`test ${nestlib}`); + expect(jestResult.stderr).toContain('Test Suites: 1 passed, 1 total'); + }, 60000); + + it('should be able to generate a nest library w/ controller and service', async () => { + ensureProject(); + const nestlib = uniq('nestlib'); + + runCLI(`generate @nrwl/nest:lib ${nestlib} --controller --service`); + + const lintResults = runCLI(`lint ${nestlib}`); + expect(lintResults).toContain('All files pass linting.'); + + const jestResult = await runCLIAsync(`test ${nestlib}`); + expect(jestResult.stderr).toContain('Test Suites: 2 passed, 2 total'); + }, 60000); + }); + it('should be able to generate an empty application', async () => { ensureProject(); const nodeapp = uniq('nodeapp'); diff --git a/packages/jest/src/schematics/jest-project/files/jest.config.js__tmpl__ b/packages/jest/src/schematics/jest-project/files/jest.config.js__tmpl__ index 20d7cea386..d1a2933a61 100644 --- a/packages/jest/src/schematics/jest-project/files/jest.config.js__tmpl__ +++ b/packages/jest/src/schematics/jest-project/files/jest.config.js__tmpl__ @@ -1,6 +1,7 @@ module.exports = { name: '<%= project %>', - preset: '<%= offsetFromRoot %>jest.config.js',<% if (supportTsx) { %> + preset: '<%= offsetFromRoot %>jest.config.js',<% if(testEnvironment) { %> + testEnvironment: '<%= testEnvironment %>',<% } %><% if (supportTsx) { %> transform: { '^.+\\.[tj]sx?$': 'ts-jest' }, diff --git a/packages/jest/src/schematics/jest-project/jest-project.ts b/packages/jest/src/schematics/jest-project/jest-project.ts index 818fbc4095..dc3d7ad0ff 100644 --- a/packages/jest/src/schematics/jest-project/jest-project.ts +++ b/packages/jest/src/schematics/jest-project/jest-project.ts @@ -1,23 +1,22 @@ import { - Rule, - Tree, - mergeWith, - chain, - url, apply, - SchematicContext, + chain, + filter, + mergeWith, move, - template, noop, - filter + Rule, + SchematicContext, + template, + Tree, + url } from '@angular-devkit/schematics'; import { - readJsonInTree, + getProjectConfig, + offsetFromRoot, updateJsonInTree, updateWorkspaceInTree } from '@nrwl/workspace'; -import { getProjectConfig, addDepsToPackageJson } from '@nrwl/workspace'; -import { offsetFromRoot } from '@nrwl/workspace'; import { join, normalize } from '@angular-devkit/core'; import init from '../init/init'; @@ -27,6 +26,7 @@ export interface JestProjectSchema { skipSetupFile: boolean; setupFile: 'angular' | 'web-components' | 'none'; skipSerializers: boolean; + testEnvironment: 'node' | 'jsdom' | ''; } function generateFiles(options: JestProjectSchema): Rule { @@ -114,6 +114,10 @@ function check(options: JestProjectSchema): Rule { } function normalizeOptions(options: JestProjectSchema): JestProjectSchema { + if (options.testEnvironment === 'jsdom') { + options.testEnvironment = ''; + } + if (!options.skipSetupFile) { return options; } diff --git a/packages/jest/src/schematics/jest-project/schema.json b/packages/jest/src/schematics/jest-project/schema.json index 0051b6ad6c..96ae1a2865 100644 --- a/packages/jest/src/schematics/jest-project/schema.json +++ b/packages/jest/src/schematics/jest-project/schema.json @@ -32,6 +32,12 @@ "type": "boolean", "description": "Setup tsx support", "default": false + }, + "testEnvironment": { + "type": "string", + "enum": ["jsdom", "node"], + "description": "The test environment for jest", + "default": "jsdom" } }, "required": [] diff --git a/packages/nest/collection.json b/packages/nest/collection.json index 23860383cc..a1d5279238 100644 --- a/packages/nest/collection.json +++ b/packages/nest/collection.json @@ -16,6 +16,13 @@ "schema": "./src/schematics/application/schema.json", "aliases": ["app"], "description": "Create a nest application" + }, + + "library": { + "factory": "./src/schematics/library/library", + "schema": "./src/schematics/library/schema.json", + "aliases": ["lib"], + "description": "Create a new nest library" } } } diff --git a/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.controller.spec.ts__tmpl__ b/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.controller.spec.ts__tmpl__ new file mode 100644 index 0000000000..30c3ea0da6 --- /dev/null +++ b/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.controller.spec.ts__tmpl__ @@ -0,0 +1,26 @@ +import { Test } from '@nestjs/testing'; +import { <%= className %>Controller } from './<%= fileName %>.controller'; +<% if(service) { %>import { <%= className %>Service } from './<%= fileName %>.service';<% } %> + +describe('<%= className %>Controller', () => { + + let controller: <%= className %>Controller; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + <% if(service) { %><%= className %>Service<% } %> + ], + controllers: [ + <%= className %>Controller + ] + }).compile(); + + controller = module.get(<%= className %>Controller); + }); + + it('should be defined', () => { + expect(controller).toBeTruthy(); + }); + +}) diff --git a/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.controller.ts__tmpl__ b/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.controller.ts__tmpl__ new file mode 100644 index 0000000000..a8ae2f5c4e --- /dev/null +++ b/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.controller.ts__tmpl__ @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +<% if(service) { %>import { <%= className %>Service } from './<%= fileName %>.service';<% } %> + +@Controller('<%= fileName %>') +export class <%= className %>Controller { + constructor(<% if(service) { %>private <%= propertyName %>Service: <%= className %>Service<% } %>) {} +} diff --git a/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.module.ts__tmpl__ b/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.module.ts__tmpl__ new file mode 100644 index 0000000000..001980c7cd --- /dev/null +++ b/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.module.ts__tmpl__ @@ -0,0 +1,18 @@ +import { Module<% if(global) { %>, Global<% } %> } from '@nestjs/common'; +<% if(service) { %>import { <%= className %>Service } from './<%= fileName %>.service';<% } %> +<% if(controller) { %>import { <%= className %>Controller } from './<%= fileName %>.controller';<% } %> + +<% if(global) { %>@Global()<% } %> +@Module({ + controllers: [ + <% if(controller) { %><%= className %>Controller<% } %> + ], + providers: [ + <% if(service) { %><%= className %>Service<% } %> + ], + exports: [ + <% if(service) { %><%= className %>Service<% } %> + ] +}) +export class <%= className %>Module { +} diff --git a/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.service.spec.ts__tmpl__ b/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.service.spec.ts__tmpl__ new file mode 100644 index 0000000000..79a67fb503 --- /dev/null +++ b/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.service.spec.ts__tmpl__ @@ -0,0 +1,22 @@ +import { Test } from '@nestjs/testing'; +import { <%= className %>Service } from './<%= fileName %>.service'; + +describe('<%= className %>Service', () => { + + let service: <%= className %>Service; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + <%= className %>Service + ] + }).compile(); + + service = module.get(<%= className %>Service); + }); + + it('should be defined', () => { + expect(service).toBeTruthy(); + }); + +}) diff --git a/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.service.ts__tmpl__ b/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.service.ts__tmpl__ new file mode 100644 index 0000000000..e7ffb40cc4 --- /dev/null +++ b/packages/nest/src/schematics/library/files/lib/src/lib/__fileName__.service.ts__tmpl__ @@ -0,0 +1,6 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class <%= className %>Service { + constructor() {} +} diff --git a/packages/nest/src/schematics/library/library.spec.ts b/packages/nest/src/schematics/library/library.spec.ts new file mode 100644 index 0000000000..ba37d4abde --- /dev/null +++ b/packages/nest/src/schematics/library/library.spec.ts @@ -0,0 +1,409 @@ +import { Tree } from '@angular-devkit/schematics'; +import { NxJson, readJsonInTree } from '@nrwl/workspace'; +import { createEmptyWorkspace, getFileContent } from '@nrwl/workspace/testing'; +import { runSchematic } from '../../utils/testing'; +import { stripIndents } from '@angular-devkit/core/src/utils/literals'; + +describe('lib', () => { + let appTree: Tree; + + beforeEach(() => { + appTree = Tree.empty(); + appTree = createEmptyWorkspace(appTree); + }); + + describe('not nested', () => { + it('should update workspace.json', async () => { + const tree = await runSchematic('lib', { name: 'myLib' }, appTree); + const workspaceJson = readJsonInTree(tree, '/workspace.json'); + expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib'); + expect(workspaceJson.projects['my-lib'].architect.build).toBeUndefined(); + expect(workspaceJson.projects['my-lib'].architect.lint).toEqual({ + builder: '@angular-devkit/build-angular:tslint', + options: { + exclude: ['**/node_modules/**', '!libs/my-lib/**'], + tsConfig: [ + 'libs/my-lib/tsconfig.lib.json', + 'libs/my-lib/tsconfig.spec.json' + ] + } + }); + expect(workspaceJson.projects['my-lib'].architect.test).toEqual({ + builder: '@nrwl/jest:jest', + options: { + jestConfig: 'libs/my-lib/jest.config.js', + tsConfig: 'libs/my-lib/tsconfig.spec.json', + passWithNoTests: true + } + }); + }); + + it('should include a controller', async () => { + const tree = await runSchematic( + 'lib', + { name: 'myLib', controller: true }, + appTree + ); + const service = getFileContent( + tree, + 'libs/my-lib/src/lib/my-lib.controller.ts' + ); + expect(service).toBeTruthy(); + }); + + it('should include a service', async () => { + const tree = await runSchematic( + 'lib', + { name: 'myLib', service: true }, + appTree + ); + const service = getFileContent( + tree, + 'libs/my-lib/src/lib/my-lib.service.ts' + ); + expect(service).toBeTruthy(); + }); + + it('should add the @Global decorator', async () => { + const tree = await runSchematic( + 'lib', + { name: 'myLib', global: true }, + appTree + ); + const module = getFileContent( + tree, + 'libs/my-lib/src/lib/my-lib.module.ts' + ); + expect(stripIndents`${module}`).toEqual( + stripIndents`import { Module, Global } from '@nestjs/common'; + + @Global() + @Module({ + controllers: [], + providers: [], + exports: [] + }) + export class MyLibModule {}` + ); + }); + + it('should provide the controller and service', async () => { + const tree = await runSchematic( + 'lib', + { name: 'myLib', service: true, controller: true }, + appTree + ); + const module = getFileContent( + tree, + 'libs/my-lib/src/lib/my-lib.module.ts' + ); + expect(stripIndents`${module}`).toEqual( + stripIndents`import { Module } from '@nestjs/common'; + import { MyLibService } from './my-lib.service'; + import { MyLibController } from './my-lib.controller'; + + @Module({ + controllers: [MyLibController], + providers: [MyLibService], + exports: [MyLibService] + }) + export class MyLibModule {}` + ); + + const controller = getFileContent( + tree, + 'libs/my-lib/src/lib/my-lib.controller.ts' + ); + expect(stripIndents`${controller}`).toEqual( + stripIndents`import { Controller } from '@nestjs/common'; + import { MyLibService } from './my-lib.service'; + + @Controller('my-lib') + export class MyLibController { + constructor(private myLibService: MyLibService) {} + }` + ); + + const barrel = getFileContent(tree, 'libs/my-lib/src/index.ts'); + expect(stripIndents`${barrel}`).toEqual( + stripIndents`export * from './lib/my-lib.module'; + export * from './lib/my-lib.service'; + export * from './lib/my-lib.controller';` + ); + }); + + it('should update nx.json', async () => { + const tree = await runSchematic( + 'lib', + { name: 'myLib', tags: 'one,two' }, + appTree + ); + const nxJson = readJsonInTree(tree, '/nx.json'); + expect(nxJson).toEqual({ + npmScope: 'proj', + projects: { + 'my-lib': { + tags: ['one', 'two'] + } + } + }); + }); + + it('should update root tsconfig.json', async () => { + const tree = await runSchematic('lib', { name: 'myLib' }, appTree); + const tsconfigJson = readJsonInTree(tree, '/tsconfig.json'); + expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([ + 'libs/my-lib/src/index.ts' + ]); + }); + + it('should create a local tsconfig.json', async () => { + const tree = await runSchematic('lib', { name: 'myLib' }, appTree); + const tsconfigJson = readJsonInTree(tree, 'libs/my-lib/tsconfig.json'); + expect(tsconfigJson).toEqual({ + extends: '../../tsconfig.json', + compilerOptions: { + types: ['node', 'jest'], + target: 'es6' + }, + include: ['**/*.ts'] + }); + }); + + it('should extend the local tsconfig.json with tsconfig.spec.json', async () => { + const tree = await runSchematic('lib', { name: 'myLib' }, appTree); + const tsconfigJson = readJsonInTree( + tree, + 'libs/my-lib/tsconfig.spec.json' + ); + expect(tsconfigJson.extends).toEqual('./tsconfig.json'); + }); + + it('should extend the local tsconfig.json with tsconfig.lib.json', async () => { + const tree = await runSchematic('lib', { name: 'myLib' }, appTree); + const tsconfigJson = readJsonInTree( + tree, + 'libs/my-lib/tsconfig.lib.json' + ); + expect(tsconfigJson.extends).toEqual('./tsconfig.json'); + }); + + it('should generate files', async () => { + const tree = await runSchematic('lib', { name: 'myLib' }, appTree); + expect(tree.exists(`libs/my-lib/jest.config.js`)).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/index.ts')).toBeTruthy(); + expect(tree.exists(`libs/my-lib/src/lib/my-lib.spec.ts`)).toBeFalsy(); + }); + }); + + describe('nested', () => { + it('should update nx.json', async () => { + const tree = await runSchematic( + 'lib', + { + name: 'myLib', + directory: 'myDir', + tags: 'one' + }, + appTree + ); + const nxJson = readJsonInTree(tree, '/nx.json'); + expect(nxJson).toEqual({ + npmScope: 'proj', + projects: { + 'my-dir-my-lib': { + tags: ['one'] + } + } + }); + + const tree2 = await runSchematic( + 'lib', + { + name: 'myLib2', + directory: 'myDir', + tags: 'one,two' + }, + tree + ); + const nxJson2 = readJsonInTree(tree2, '/nx.json'); + expect(nxJson2).toEqual({ + npmScope: 'proj', + projects: { + 'my-dir-my-lib': { + tags: ['one'] + }, + 'my-dir-my-lib2': { + tags: ['one', 'two'] + } + } + }); + }); + + it('should generate files', async () => { + const tree = await runSchematic( + 'lib', + { name: 'myLib', directory: 'myDir' }, + appTree + ); + expect(tree.exists(`libs/my-dir/my-lib/jest.config.js`)).toBeTruthy(); + expect(tree.exists('libs/my-dir/my-lib/src/index.ts')).toBeTruthy(); + expect( + tree.exists(`libs/my-dir/my-lib/src/lib/my-lib.spec.ts`) + ).toBeFalsy(); + }); + + it('should update workspace.json', async () => { + const tree = await runSchematic( + 'lib', + { name: 'myLib', directory: 'myDir' }, + appTree + ); + const workspaceJson = readJsonInTree(tree, '/workspace.json'); + + expect(workspaceJson.projects['my-dir-my-lib'].root).toEqual( + 'libs/my-dir/my-lib' + ); + expect(workspaceJson.projects['my-dir-my-lib'].architect.lint).toEqual({ + builder: '@angular-devkit/build-angular:tslint', + options: { + exclude: ['**/node_modules/**', '!libs/my-dir/my-lib/**'], + tsConfig: [ + 'libs/my-dir/my-lib/tsconfig.lib.json', + 'libs/my-dir/my-lib/tsconfig.spec.json' + ] + } + }); + }); + + it('should update tsconfig.json', async () => { + const tree = await runSchematic( + 'lib', + { name: 'myLib', directory: 'myDir' }, + appTree + ); + const tsconfigJson = readJsonInTree(tree, '/tsconfig.json'); + expect(tsconfigJson.compilerOptions.paths['@proj/my-dir/my-lib']).toEqual( + ['libs/my-dir/my-lib/src/index.ts'] + ); + expect( + tsconfigJson.compilerOptions.paths['my-dir-my-lib/*'] + ).toBeUndefined(); + }); + + it('should create a local tsconfig.json', async () => { + const tree = await runSchematic( + 'lib', + { name: 'myLib', directory: 'myDir' }, + appTree + ); + + const tsconfigJson = readJsonInTree( + tree, + 'libs/my-dir/my-lib/tsconfig.json' + ); + expect(tsconfigJson).toEqual({ + extends: '../../../tsconfig.json', + compilerOptions: { + types: ['node', 'jest'], + target: 'es6' + }, + include: ['**/*.ts'] + }); + }); + }); + + describe('--unit-test-runner none', () => { + it('should not generate test configuration', async () => { + const resultTree = await runSchematic( + 'lib', + { name: 'myLib', unitTestRunner: 'none' }, + appTree + ); + expect(resultTree.exists('libs/my-lib/tsconfig.spec.json')).toBeFalsy(); + expect(resultTree.exists('libs/my-lib/jest.config.js')).toBeFalsy(); + expect(resultTree.exists('libs/my-lib/lib/my-lib.spec.ts')).toBeFalsy(); + const workspaceJson = readJsonInTree(resultTree, 'workspace.json'); + expect(workspaceJson.projects['my-lib'].architect.test).toBeUndefined(); + const tsconfigJson = readJsonInTree( + resultTree, + 'libs/my-lib/tsconfig.json' + ); + expect(tsconfigJson).toEqual({ + extends: '../../tsconfig.json', + compilerOptions: { + types: ['node'], + target: 'es6' + }, + include: ['**/*.ts'] + }); + expect( + workspaceJson.projects['my-lib'].architect.lint.options.tsConfig + ).toEqual(['libs/my-lib/tsconfig.lib.json']); + }); + }); + + describe('publishable package', () => { + it('should update package.json', async () => { + const publishableTree = await runSchematic( + 'lib', + { name: 'mylib', publishable: true }, + appTree + ); + + let packageJsonContent = readJsonInTree( + publishableTree, + 'libs/mylib/package.json' + ); + + expect(packageJsonContent.name).toEqual('@proj/mylib'); + }); + }); + + describe('compiler options target', () => { + it('should set target to es6 in tsconfig.json by default', async () => { + const tree = await runSchematic( + 'lib', + { name: 'myLib', directory: 'myDir' }, + appTree + ); + + const tsconfigJson = readJsonInTree( + tree, + 'libs/my-dir/my-lib/tsconfig.json' + ); + expect(tsconfigJson.compilerOptions.target).toEqual('es6'); + }); + + it('should set target to es2020 in tsconfig.json', async () => { + const tree = await runSchematic( + 'lib', + { name: 'myLib', directory: 'myDir', target: 'es2020' }, + appTree + ); + + const tsconfigJson = readJsonInTree( + tree, + 'libs/my-dir/my-lib/tsconfig.json' + ); + expect(tsconfigJson.compilerOptions.target).toEqual('es2020'); + }); + + it('should set target jest testEnvironment to node', async () => { + const tree = await runSchematic('lib', { name: 'myLib' }, appTree); + + const jestConfig = getFileContent(tree, 'libs/my-lib/jest.config.js'); + expect(stripIndents`${jestConfig}`) + .toEqual(stripIndents`module.exports = { + name: 'my-lib', + preset: '../../jest.config.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]sx?$': 'ts-jest' + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], + coverageDirectory: '../../coverage/libs/my-lib' + };`); + }); + }); +}); diff --git a/packages/nest/src/schematics/library/library.ts b/packages/nest/src/schematics/library/library.ts new file mode 100644 index 0000000000..e27df27f4d --- /dev/null +++ b/packages/nest/src/schematics/library/library.ts @@ -0,0 +1,192 @@ +import { normalize, Path } from '@angular-devkit/core'; +import { + apply, + chain, + externalSchematic, + filter, + MergeStrategy, + mergeWith, + move, + noop, + Rule, + SchematicContext, + template, + Tree, + url +} from '@angular-devkit/schematics'; +import { + addGlobal, + deleteFile, + formatFiles, + getNpmScope, + getProjectConfig, + insert, + names, + offsetFromRoot, + toFileName, + updateJsonInTree, + updateWorkspaceInTree +} from '@nrwl/workspace'; +import { Schema } from './schema'; +import * as ts from 'typescript'; +import { RemoveChange } from '@nrwl/workspace/src/utils/ast-utils'; + +export interface NormalizedSchema extends Schema { + name: string; + prefix: string; + fileName: string; + projectRoot: Path; + projectDirectory: string; + parsedTags: string[]; +} + +export default function(schema: NormalizedSchema): Rule { + return (host: Tree, context: SchematicContext) => { + const options = normalizeOptions(host, schema); + + return chain([ + externalSchematic('@nrwl/node', 'lib', schema), + createFiles(options), + addExportsToBarrelFile(options), + updateTsConfig(options), + addProject(options), + formatFiles(options), + deleteFile(`/${options.projectRoot}/src/lib/${options.fileName}.spec.ts`) + ]); + }; +} + +function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { + const defaultPrefix = getNpmScope(host); + 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 normalized: NormalizedSchema = { + ...options, + prefix: defaultPrefix, // we could also allow customizing this + fileName, + name: projectName, + projectRoot, + projectDirectory, + parsedTags + }; + + return normalized; +} + +function addExportsToBarrelFile(options: NormalizedSchema): Rule { + return (host: Tree) => { + const indexFilePath = `${options.projectRoot}/src/index.ts`; + const buffer = host.read(indexFilePath); + if (!!buffer) { + const indexSource = buffer!.toString('utf-8'); + const indexSourceFile = ts.createSourceFile( + indexFilePath, + indexSource, + ts.ScriptTarget.Latest, + true + ); + + insert(host, indexFilePath, [ + new RemoveChange( + indexFilePath, + 0, + `export * from './lib/${options.fileName}';` + ), + ...addGlobal( + indexSourceFile, + indexFilePath, + `export * from './lib/${options.fileName}.module';` + ), + ...(options.service + ? addGlobal( + indexSourceFile, + indexFilePath, + `export * from './lib/${options.fileName}.service';` + ) + : []), + ...(options.controller + ? addGlobal( + indexSourceFile, + indexFilePath, + `export * from './lib/${options.fileName}.controller';` + ) + : []) + ]); + } + }; +} + +function createFiles(options: NormalizedSchema): Rule { + return mergeWith( + apply(url(`./files/lib`), [ + template({ + ...options, + ...names(options.name), + tmpl: '', + offsetFromRoot: offsetFromRoot(options.projectRoot) + }), + move(options.projectRoot), + options.unitTestRunner === 'none' + ? filter(file => !file.endsWith('spec.ts')) + : noop(), + options.publishable + ? noop() + : filter(file => !file.endsWith('package.json')), + options.service ? noop() : filter(file => !file.endsWith('.service.ts')), + options.controller + ? noop() + : filter(file => !file.endsWith('.controller.ts')), + !options.controller || options.unitTestRunner === 'none' + ? filter(file => !file.endsWith('.controller.spec.ts')) + : noop(), + !options.service || options.unitTestRunner === 'none' + ? filter(file => !file.endsWith('.service.spec.ts')) + : noop() + ]), + MergeStrategy.Overwrite + ); +} + +function updateTsConfig(options: NormalizedSchema): Rule { + return (host: Tree, context: SchematicContext) => { + const projectConfig = getProjectConfig(host, options.name); + return updateJsonInTree(`${projectConfig.root}/tsconfig.json`, json => { + json.compilerOptions.target = options.target; + return json; + }); + }; +} + +function addProject(options: NormalizedSchema): Rule { + if (!options.publishable) { + return noop(); + } + + return updateWorkspaceInTree(json => { + const architect = json.projects[options.name].architect; + if (architect) { + architect.build = { + builder: '@nrwl/node:package', + options: { + outputPath: `dist/libs/${options.projectDirectory}`, + tsConfig: `${options.projectRoot}/tsconfig.lib.json`, + packageJson: `${options.projectRoot}/package.json`, + main: `${options.projectRoot}/src/index.ts`, + assets: [`${options.projectRoot}/*.md`] + } + }; + } + return json; + }); +} diff --git a/packages/nest/src/schematics/library/schema.d.ts b/packages/nest/src/schematics/library/schema.d.ts new file mode 100644 index 0000000000..b3b84628f5 --- /dev/null +++ b/packages/nest/src/schematics/library/schema.d.ts @@ -0,0 +1,26 @@ +import { Linter } from '@nrwl/workspace'; + +export interface Schema { + name: string; + directory?: string; + skipTsConfig: boolean; + skipFormat: boolean; + tags?: string; + unitTestRunner: 'jest' | 'none'; + linter: Linter; + publishable?: boolean; + global?: boolean; + service?: boolean; + controller?: boolean; + target?: + | 'es5' + | 'es6' + | 'esnext' + | 'es2015' + | 'es2016' + | 'es2017' + | 'es2018' + | 'es2019' + | 'es2020'; + testEnvironment: 'jsdom' | 'node'; +} diff --git a/packages/nest/src/schematics/library/schema.json b/packages/nest/src/schematics/library/schema.json new file mode 100644 index 0000000000..34afa3a3d8 --- /dev/null +++ b/packages/nest/src/schematics/library/schema.json @@ -0,0 +1,97 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "NxNestLibrary", + "title": "Create a Nest Library for Nx", + "type": "object", + "examples": [ + { + "command": "g lib mylib --directory=myapp", + "description": "Generate libs/myapp/mylib" + } + ], + "properties": { + "name": { + "type": "string", + "description": "Library name", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use for the library?" + }, + "directory": { + "type": "string", + "description": "A directory where the app 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." + }, + "publishable": { + "type": "boolean", + "description": "Create a publishable library. A \"build\" architect will be added for this project the workspace configuration." + }, + "global": { + "type": "boolean", + "description": "Add the Global decorator to the generated module.", + "default": false + }, + "service": { + "type": "boolean", + "description": "Include a service with the library.", + "default": false + }, + "controller": { + "type": "boolean", + "description": "Include a controller with the library", + "default": false + }, + "testEnvironment": { + "type": "string", + "enum": ["jsdom", "node"], + "description": "The test environment for jest, for node applications this should stay as node unless doing DOM testing.", + "default": "node" + }, + "target": { + "type": "string", + "description": "The es target, Nest suggest using es6 or higher.", + "default": "es6", + "enum": [ + "es5", + "es6", + "esnext", + "es2015", + "es2016", + "es2017", + "es2018", + "es2019", + "es2020" + ] + } + }, + "required": ["name"] +} diff --git a/packages/node/src/schematics/library/schema.d.ts b/packages/node/src/schematics/library/schema.d.ts index d426ea324d..12b6d7761d 100644 --- a/packages/node/src/schematics/library/schema.d.ts +++ b/packages/node/src/schematics/library/schema.d.ts @@ -9,4 +9,5 @@ export interface Schema { unitTestRunner: 'jest' | 'none'; linter: Linter; publishable?: boolean; + testEnvironment: 'jsdom' | 'node'; } diff --git a/packages/node/src/schematics/library/schema.json b/packages/node/src/schematics/library/schema.json index 31c8abd496..820b8c7e50 100644 --- a/packages/node/src/schematics/library/schema.json +++ b/packages/node/src/schematics/library/schema.json @@ -54,6 +54,12 @@ "publishable": { "type": "boolean", "description": "Create a publishable library. A \"build\" architect will be added for this project the workspace configuration." + }, + "testEnvironment": { + "type": "string", + "enum": ["jsdom", "node"], + "description": "The test environment to use if unitTestRunner is set to jest", + "default": "jsdom" } }, "required": ["name"] diff --git a/packages/workspace/src/schematics/library/library.ts b/packages/workspace/src/schematics/library/library.ts index 174d69fc0b..4e14ecd1ec 100644 --- a/packages/workspace/src/schematics/library/library.ts +++ b/packages/workspace/src/schematics/library/library.ts @@ -100,7 +100,8 @@ export default function(schema: Schema): Rule { project: options.name, setupFile: 'none', supportTsx: true, - skipSerializers: true + skipSerializers: true, + testEnvironment: options.testEnvironment }) : noop(), formatFiles(options) diff --git a/packages/workspace/src/schematics/library/schema.d.ts b/packages/workspace/src/schematics/library/schema.d.ts index b4f1132b02..876f938e7e 100644 --- a/packages/workspace/src/schematics/library/schema.d.ts +++ b/packages/workspace/src/schematics/library/schema.d.ts @@ -9,4 +9,5 @@ export interface Schema { simpleModuleName: boolean; unitTestRunner: 'jest' | 'none'; linter: Linter; + testEnvironment: 'jsdom' | 'node'; } diff --git a/packages/workspace/src/schematics/library/schema.json b/packages/workspace/src/schematics/library/schema.json index f6170d92b0..e46958ef80 100644 --- a/packages/workspace/src/schematics/library/schema.json +++ b/packages/workspace/src/schematics/library/schema.json @@ -48,6 +48,12 @@ "type": "boolean", "default": false, "description": "Do not update tsconfig.json for development experience." + }, + "testEnvironment": { + "type": "string", + "enum": ["jsdom", "node"], + "description": "The test environment to use if unitTestRunner is set to jest", + "default": "jsdom" } }, "required": ["name"] diff --git a/packages/workspace/src/schematics/shared-new/shared-new.ts b/packages/workspace/src/schematics/shared-new/shared-new.ts index 3cab39c862..a3ff66cc19 100644 --- a/packages/workspace/src/schematics/shared-new/shared-new.ts +++ b/packages/workspace/src/schematics/shared-new/shared-new.ts @@ -253,7 +253,10 @@ function setDefaultLinter(linter: string) { plugin: { linter } }; json.schematics['@nrwl/nest'] = { application: { linter } }; - json.schematics['@nrwl/express'] = { application: { linter } }; + json.schematics['@nrwl/express'] = { + application: { linter }, + library: { linter } + }; return json; }); } diff --git a/scripts/commit-lint.js b/scripts/commit-lint.js index 65ee61ca37..d721e6c8c6 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|linter|node|nx-plugin|react|storybook|testing|repo|misc)\):\s(([a-z0-9:\-\s])+)/g.test( +const matchCommit = /(chore|feat|fix|cleanup|docs)\((angular|bazel|core|docs|nextjs|nest|linter|node|nx-plugin|react|storybook|testing|repo|misc)\):\s(([a-z0-9:\-\s])+)/g.test( gitMessage ); const matchRevert = /Revert/gi.test(gitMessage); @@ -27,7 +27,7 @@ if (exitCode === 0) { console.log('\n'); console.log('possible types: chore|build|feat|fix|cleanup|docs'); console.log( - 'possible scopes: angular|bazel|core|docs|nextjs|linter|node|react|storybook|testing|repo|misc (if unsure use "core")' + 'possible scopes: angular|bazel|core|docs|nextjs|nest|linter|linter|node|react|storybook|testing|repo|misc (if unsure use "core")' ); console.log( '\nEXAMPLE: \n' +