diff --git a/docs/generated/packages/node.json b/docs/generated/packages/node.json index 71e6fb4796..b0b3126583 100644 --- a/docs/generated/packages/node.json +++ b/docs/generated/packages/node.json @@ -138,6 +138,136 @@ "implementation": "/packages/node/src/generators/application/application.ts", "hidden": false, "path": "/packages/node/src/generators/application/schema.json" + }, + { + "name": "library", + "factory": "./src/generators/library/library", + "schema": { + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "NxNodeLibrary", + "title": "Create a Node Library for Nx", + "description": "Create a Node Library for an Nx workspace.", + "type": "object", + "examples": [ + { + "command": "nx 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 lib is placed", + "alias": "dir" + }, + "simpleModuleName": { + "description": "Keep the module name simple (when using `--directory`).", + "type": "boolean", + "default": false + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint"], + "default": "eslint" + }, + "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.base.json` for development experience." + }, + "publishable": { + "type": "boolean", + "description": "Create a publishable library." + }, + "buildable": { + "type": "boolean", + "default": false, + "description": "Generate a buildable library." + }, + "compiler": { + "type": "string", + "enum": ["tsc", "swc"], + "default": "tsc", + "description": "The compiler used by the build and test targets." + }, + "importPath": { + "type": "string", + "description": "The library name used to import it, like `@myorg/my-awesome-lib`. Must be a valid npm name." + }, + "rootDir": { + "type": "string", + "description": "Sets the `rootDir` for TypeScript compilation. When not defined, it uses the project's root property, or `srcRootForCompilationRoot` if it is defined." + }, + "testEnvironment": { + "type": "string", + "enum": ["jsdom", "node"], + "description": "The test environment to use if `unitTestRunner` is set to `jest`.", + "default": "jsdom" + }, + "babelJest": { + "type": "boolean", + "description": "Use `babel` instead of `ts-jest`.", + "default": false + }, + "pascalCaseFiles": { + "type": "boolean", + "description": "Use pascal case file names.", + "alias": "P", + "default": false + }, + "js": { + "type": "boolean", + "description": "Generate JavaScript files rather than TypeScript files.", + "default": false + }, + "strict": { + "type": "boolean", + "description": "Whether to enable tsconfig strict mode or not.", + "default": false + }, + "standaloneConfig": { + "description": "Split the project configuration into `/project.json` rather than including it inside `workspace.json`.", + "type": "boolean" + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint `parserOptions.project`. We do not do this by default for lint performance reasons.", + "default": false + } + }, + "required": ["name"], + "presets": [] + }, + "aliases": ["lib"], + "x-type": "library", + "description": "Create a node library.", + "implementation": "/packages/node/src/generators/library/library.ts", + "hidden": false, + "path": "/packages/node/src/generators/library/schema.json" } ], "executors": [ diff --git a/docs/packages.json b/docs/packages.json index 15b125c0e8..05953b238e 100644 --- a/docs/packages.json +++ b/docs/packages.json @@ -248,7 +248,7 @@ "path": "generated/packages/node.json", "schemas": { "executors": ["webpack", "node"], - "generators": ["init", "application"] + "generators": ["init", "application", "library"] } }, { diff --git a/nx-dev/nx-dev-e2e/src/integration/packages.spec.ts b/nx-dev/nx-dev-e2e/src/integration/packages.spec.ts index 7f6d5b6adf..2a2735dec2 100644 --- a/nx-dev/nx-dev-e2e/src/integration/packages.spec.ts +++ b/nx-dev/nx-dev-e2e/src/integration/packages.spec.ts @@ -296,6 +296,7 @@ describe('nx-dev: Packages Section', () => { title: '@nrwl/node:application', path: '/packages/node/generators/application', }, + { title: '@nrwl/node:library', path: '/packages/node/generators/library' }, { title: '@nrwl/node:webpack', path: '/packages/node/executors/webpack' }, { title: '@nrwl/node:node', path: '/packages/node/executors/node' }, { title: 'nx', path: '/packages/nx' }, diff --git a/packages/node/generators.json b/packages/node/generators.json index 6ff3796c18..77bc307983 100644 --- a/packages/node/generators.json +++ b/packages/node/generators.json @@ -1,7 +1,7 @@ { "name": "nx/node", "version": "0.1", - "extends": ["@nrwl/js"], + "extends": ["@nrwl/workspace"], "generators": { "init": { "factory": "./src/generators/init/init", @@ -16,6 +16,13 @@ "aliases": ["app"], "x-type": "application", "description": "Create a node application." + }, + "library": { + "factory": "./src/generators/library/library", + "schema": "./src/generators/library/schema.json", + "aliases": ["lib"], + "x-type": "library", + "description": "Create a node library." } }, "schematics": { @@ -32,6 +39,13 @@ "aliases": ["app"], "x-type": "application", "description": "Create a node application." + }, + "library": { + "factory": "./src/generators/library/library#librarySchematic", + "schema": "./src/generators/library/schema.json", + "aliases": ["lib"], + "x-type": "library", + "description": "Create a node library." } } } diff --git a/packages/node/index.ts b/packages/node/index.ts index 17f0a02049..642dd5b126 100644 --- a/packages/node/index.ts +++ b/packages/node/index.ts @@ -1,8 +1,3 @@ -import { libraryGenerator } from '@nrwl/js'; - -// backwards compat -// TODO(jack): Remove in Nx 16 -export { libraryGenerator }; - export { applicationGenerator } from './src/generators/application/application'; +export { libraryGenerator } from './src/generators/library/library'; export { initGenerator } from './src/generators/init/init'; diff --git a/packages/node/src/generators/library/files/lib/package.json__tmpl__ b/packages/node/src/generators/library/files/lib/package.json__tmpl__ new file mode 100644 index 0000000000..e3a3ad83c4 --- /dev/null +++ b/packages/node/src/generators/library/files/lib/package.json__tmpl__ @@ -0,0 +1,4 @@ +{ + "name": "<%= importPath %>", + "version": "0.0.1" +} diff --git a/packages/node/src/generators/library/files/lib/src/lib/__fileName__.spec.ts__tmpl__ b/packages/node/src/generators/library/files/lib/src/lib/__fileName__.spec.ts__tmpl__ new file mode 100644 index 0000000000..35b0948b95 --- /dev/null +++ b/packages/node/src/generators/library/files/lib/src/lib/__fileName__.spec.ts__tmpl__ @@ -0,0 +1,7 @@ +import { <%= propertyName %> } from './<%= fileName %>'; + +describe('<%= propertyName %>', () => { + it('should work', () => { + expect(<%= propertyName %>()).toEqual('<%= name %>'); + }) +}) \ No newline at end of file diff --git a/packages/node/src/generators/library/files/lib/src/lib/__fileName__.ts__tmpl__ b/packages/node/src/generators/library/files/lib/src/lib/__fileName__.ts__tmpl__ new file mode 100644 index 0000000000..87f0f45f16 --- /dev/null +++ b/packages/node/src/generators/library/files/lib/src/lib/__fileName__.ts__tmpl__ @@ -0,0 +1,3 @@ +export function <%= propertyName %>(): string { + return '<%= name %>'; +} \ No newline at end of file diff --git a/packages/node/src/generators/library/files/lib/tsconfig.lib.json b/packages/node/src/generators/library/files/lib/tsconfig.lib.json new file mode 100644 index 0000000000..fc6648159e --- /dev/null +++ b/packages/node/src/generators/library/files/lib/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "<%= offsetFromRoot %>dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"], + "include": ["**/*.ts"] +} diff --git a/packages/node/src/generators/library/library.spec.ts b/packages/node/src/generators/library/library.spec.ts new file mode 100644 index 0000000000..d986f20539 --- /dev/null +++ b/packages/node/src/generators/library/library.spec.ts @@ -0,0 +1,582 @@ +import { + getProjects, + NxJsonConfiguration, + readJson, + readProjectConfiguration, + Tree, +} from '@nrwl/devkit'; +import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; + +import { Schema } from './schema.d'; +import { libraryGenerator } from './library'; + +const baseLibraryConfig = { + name: 'myLib', + standaloneConfig: false, + compiler: 'tsc' as const, +}; + +describe('lib', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyV1Workspace(); + }); + + describe('not nested', () => { + it('should update workspace.json', async () => { + await libraryGenerator(tree, baseLibraryConfig); + const workspaceJson = readJson(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: '@nrwl/linter:eslint', + outputs: ['{options.outputFile}'], + options: { + lintFilePatterns: ['libs/my-lib/**/*.ts'], + }, + }); + expect(workspaceJson.projects['my-lib'].architect.test).toEqual({ + builder: '@nrwl/jest:jest', + outputs: ['{workspaceRoot}/coverage/{projectRoot}'], + options: { + jestConfig: 'libs/my-lib/jest.config.ts', + passWithNoTests: true, + }, + }); + }); + + it('adds srcRootForCompilationRoot in workspace.json', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + rootDir: './src', + buildable: true, + }); + const workspaceJson = readJson(tree, '/workspace.json'); + expect( + workspaceJson.projects['my-lib'].architect.build.options + .srcRootForCompilationRoot + ).toEqual('./src'); + }); + + it('should update tags', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + tags: 'one,two', + }); + const projects = Object.fromEntries(getProjects(tree)); + expect(projects).toMatchObject({ + 'my-lib': { + tags: ['one', 'two'], + }, + }); + }); + + it('should update root tsconfig.base.json', async () => { + await libraryGenerator(tree, baseLibraryConfig); + const tsconfigJson = readJson(tree, '/tsconfig.base.json'); + expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([ + 'libs/my-lib/src/index.ts', + ]); + }); + + it('should create a local tsconfig.json', async () => { + await libraryGenerator(tree, baseLibraryConfig); + const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.json'); + expect(tsconfigJson).toMatchInlineSnapshot(` + Object { + "extends": "../../tsconfig.base.json", + "files": Array [], + "include": Array [], + "references": Array [ + Object { + "path": "./tsconfig.lib.json", + }, + Object { + "path": "./tsconfig.spec.json", + }, + ], + } + `); + }); + + it('should extend the local tsconfig.json with tsconfig.spec.json', async () => { + await libraryGenerator(tree, baseLibraryConfig); + const tsconfigJson = readJson(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 () => { + await libraryGenerator(tree, baseLibraryConfig); + const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.lib.json'); + expect(tsconfigJson.compilerOptions.types).toContain('node'); + expect(tsconfigJson.extends).toEqual('./tsconfig.json'); + }); + + it('should exclude test files from tsconfig.lib.json', async () => { + await libraryGenerator(tree, baseLibraryConfig); + const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.lib.json'); + expect(tsconfigJson.exclude).toEqual([ + 'jest.config.ts', + '**/*.spec.ts', + '**/*.test.ts', + ]); + }); + + it('should generate files', async () => { + await libraryGenerator(tree, baseLibraryConfig); + expect(tree.exists(`libs/my-lib/jest.config.ts`)).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/index.ts')).toBeTruthy(); + + const eslintrc = readJson(tree, 'libs/my-lib/.eslintrc.json'); + expect(eslintrc).toMatchInlineSnapshot(` + Object { + "extends": Array [ + "../../.eslintrc.json", + ], + "ignorePatterns": Array [ + "!**/*", + ], + "overrides": Array [ + Object { + "files": Array [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx", + ], + "rules": Object {}, + }, + Object { + "files": Array [ + "*.ts", + "*.tsx", + ], + "rules": Object {}, + }, + Object { + "files": Array [ + "*.js", + "*.jsx", + ], + "rules": Object {}, + }, + ], + } + `); + }); + }); + + describe('nested', () => { + it('should update tags', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + directory: 'myDir', + tags: 'one', + }); + let projects = Object.fromEntries(getProjects(tree)); + expect(projects).toMatchObject({ + 'my-dir-my-lib': { + tags: ['one'], + }, + }); + + await libraryGenerator(tree, { + ...baseLibraryConfig, + name: 'myLib2', + directory: 'myDir', + tags: 'one,two', + }); + projects = Object.fromEntries(getProjects(tree)); + expect(projects).toMatchObject({ + 'my-dir-my-lib': { + tags: ['one'], + }, + 'my-dir-my-lib2': { + tags: ['one', 'two'], + }, + }); + }); + + it('should generate files', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + directory: 'myDir', + }); + expect(tree.exists(`libs/my-dir/my-lib/jest.config.ts`)).toBeTruthy(); + expect(tree.exists('libs/my-dir/my-lib/src/index.ts')).toBeTruthy(); + }); + + it('should update workspace.json', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + directory: 'myDir', + }); + const workspaceJson = readJson(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: '@nrwl/linter:eslint', + outputs: ['{options.outputFile}'], + options: { + lintFilePatterns: ['libs/my-dir/my-lib/**/*.ts'], + }, + }); + }); + + it('should update tsconfig.json', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + directory: 'myDir', + }); + const tsconfigJson = readJson(tree, '/tsconfig.base.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 throw an exception when not passing importPath when using --publishable', async () => { + expect.assertions(1); + + try { + await libraryGenerator(tree, { + ...baseLibraryConfig, + directory: 'myDir', + publishable: true, + }); + } catch (e) { + expect(e.message).toContain( + 'For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)' + ); + } + }); + + it('should create a local tsconfig.json', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + directory: 'myDir', + }); + + const tsconfigJson = readJson(tree, 'libs/my-dir/my-lib/tsconfig.json'); + expect(tsconfigJson.extends).toEqual('../../../tsconfig.base.json'); + expect(tsconfigJson.references).toEqual([ + { + path: './tsconfig.lib.json', + }, + { + path: './tsconfig.spec.json', + }, + ]); + }); + + it('should generate filenames that do not contain directory with --simpleModuleName', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + directory: 'myDir', + simpleModuleName: true, + }); + expect(tree.exists(`libs/my-dir/my-lib/jest.config.ts`)).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.ts')).toBeTruthy(); + expect( + tree.exists('libs/my-dir/my-lib/src/lib/my-lib.spec.ts') + ).toBeTruthy(); + expect(tree.exists('libs/my-dir/my-lib/src/index.ts')).toBeTruthy(); + expect(tree.exists(`libs/my-dir/my-lib/.eslintrc.json`)).toBeTruthy(); + }); + }); + + describe('--compiler', () => { + it('should specify tsc as compiler', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + compiler: 'tsc', + buildable: true, + }); + + const { build } = readProjectConfiguration(tree, 'my-lib').targets; + + expect(build.executor).toEqual('@nrwl/js:tsc'); + }); + + it('should specify swc as compiler', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + compiler: 'swc', + buildable: true, + }); + + const { build } = readProjectConfiguration(tree, 'my-lib').targets; + + expect(build.executor).toEqual('@nrwl/js:swc'); + }); + }); + + describe('--unit-test-runner none', () => { + it('should not generate test configuration', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + unitTestRunner: 'none', + }); + expect(tree.exists('libs/my-lib/tsconfig.spec.json')).toBeFalsy(); + expect(tree.exists('libs/my-lib/jest.config.ts')).toBeFalsy(); + expect(tree.exists('libs/my-lib/lib/my-lib.spec.ts')).toBeFalsy(); + const workspaceJson = readJson(tree, 'workspace.json'); + expect(workspaceJson.projects['my-lib'].architect.test).toBeUndefined(); + const tsconfigJson = readJson(tree, 'libs/my-lib/tsconfig.json'); + expect(tsconfigJson.extends).toEqual('../../tsconfig.base.json'); + expect(tsconfigJson.references).toEqual([ + { + path: './tsconfig.lib.json', + }, + ]); + expect(workspaceJson.projects['my-lib'].architect.lint) + .toMatchInlineSnapshot(` + Object { + "builder": "@nrwl/linter:eslint", + "options": Object { + "lintFilePatterns": Array [ + "libs/my-lib/**/*.ts", + ], + }, + "outputs": Array [ + "{options.outputFile}", + ], + } + `); + }); + }); + + describe('buildable package', () => { + it('should have a builder defined', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + buildable: true, + }); + const workspaceJson = readJson(tree, '/workspace.json'); + + expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib'); + + expect(workspaceJson.projects['my-lib'].architect.build) + .toMatchInlineSnapshot(` + Object { + "builder": "@nrwl/js:tsc", + "options": Object { + "assets": Array [ + "libs/my-lib/*.md", + ], + "main": "libs/my-lib/src/index.ts", + "outputPath": "dist/libs/my-lib", + "packageJson": "libs/my-lib/package.json", + "tsConfig": "libs/my-lib/tsconfig.lib.json", + }, + "outputs": Array [ + "{options.outputPath}", + ], + } + `); + }); + }); + + describe('publishable package', () => { + it('should have a builder defined', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + publishable: true, + importPath: '@proj/mylib', + }); + const workspaceJson = readJson(tree, '/workspace.json'); + + expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib'); + + expect(workspaceJson.projects['my-lib'].architect.build).toBeDefined(); + }); + + it('should update package.json', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + name: 'mylib', + publishable: true, + importPath: '@proj/mylib', + }); + + let packageJsonContent = readJson(tree, 'libs/mylib/package.json'); + + expect(packageJsonContent.name).toEqual('@proj/mylib'); + }); + }); + + describe('--importPath', () => { + it('should update the package.json & tsconfig with the given import path', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + publishable: true, + directory: 'myDir', + importPath: '@myorg/lib', + }); + const packageJson = readJson(tree, 'libs/my-dir/my-lib/package.json'); + const tsconfigJson = readJson(tree, '/tsconfig.base.json'); + + expect(packageJson.name).toBe('@myorg/lib'); + expect( + tsconfigJson.compilerOptions.paths[packageJson.name] + ).toBeDefined(); + }); + + it('should fail if the same importPath has already been used', async () => { + await libraryGenerator(tree, { + ...baseLibraryConfig, + name: 'myLib1', + publishable: true, + importPath: '@myorg/lib', + }); + + try { + await libraryGenerator(tree, { + ...baseLibraryConfig, + name: 'myLib2', + publishable: true, + importPath: '@myorg/lib', + }); + } catch (e) { + expect(e.message).toContain( + 'You already have a library using the import path' + ); + } + + expect.assertions(1); + }); + }); + + describe(`--babelJest`, () => { + it('should use babel for jest', async () => { + await libraryGenerator(tree, { + name: 'myLib', + babelJest: true, + } as Schema); + + expect(tree.read(`libs/my-lib/jest.config.ts`, 'utf-8')) + .toMatchInlineSnapshot(` + "/* eslint-disable */ + export default { + displayName: 'my-lib', + preset: '../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\\\\\\\.[tj]sx?$': 'babel-jest' + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/my-lib' + }; + " + `); + }); + }); + describe('--js flag', () => { + it('should generate js files instead of ts files', async () => { + await libraryGenerator(tree, { + name: 'myLib', + js: true, + } as Schema); + + expect(tree.exists(`libs/my-lib/jest.config.js`)).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/index.js')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/my-lib.js')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/my-lib.spec.js')).toBeTruthy(); + + expect( + readJson(tree, 'libs/my-lib/tsconfig.json').compilerOptions + ).toEqual({ + allowJs: true, + }); + expect(readJson(tree, 'libs/my-lib/tsconfig.lib.json').include).toEqual([ + '**/*.ts', + '**/*.js', + ]); + expect(readJson(tree, 'libs/my-lib/tsconfig.lib.json').exclude).toEqual([ + 'jest.config.ts', + '**/*.spec.ts', + '**/*.test.ts', + '**/*.spec.js', + '**/*.test.js', + ]); + }); + + it('should update root tsconfig.json with a js file path', async () => { + await libraryGenerator(tree, { name: 'myLib', js: true } as Schema); + const tsconfigJson = readJson(tree, '/tsconfig.base.json'); + expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([ + 'libs/my-lib/src/index.js', + ]); + }); + + it('should update architect builder when --buildable', async () => { + await libraryGenerator(tree, { + name: 'myLib', + buildable: true, + js: true, + } as Schema); + const workspaceJson = readJson(tree, '/workspace.json'); + + expect(workspaceJson.projects['my-lib'].root).toEqual('libs/my-lib'); + + expect( + workspaceJson.projects['my-lib'].architect.build.options.main + ).toEqual('libs/my-lib/src/index.js'); + }); + + it('should generate js files for nested libs as well', async () => { + await libraryGenerator(tree, { + name: 'myLib', + directory: 'myDir', + js: true, + } as Schema); + expect(tree.exists(`libs/my-dir/my-lib/jest.config.js`)).toBeTruthy(); + expect(tree.exists('libs/my-dir/my-lib/src/index.js')).toBeTruthy(); + expect( + tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.js') + ).toBeTruthy(); + expect( + tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.spec.js') + ).toBeTruthy(); + }); + }); + + describe('--pascalCaseFiles', () => { + it('should generate files with upper case names', async () => { + await libraryGenerator(tree, { + name: 'myLib', + pascalCaseFiles: true, + } as Schema); + expect(tree.exists('libs/my-lib/src/lib/MyLib.ts')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/MyLib.spec.ts')).toBeTruthy(); + expect(tree.exists('libs/my-lib/src/lib/my-lib.spec.ts')).toBeFalsy(); + expect(tree.exists('libs/my-lib/src/lib/my-lib.ts')).toBeFalsy(); + }); + + it('should generate files with upper case names for nested libs as well', async () => { + await libraryGenerator(tree, { + name: 'myLib', + directory: 'myDir', + pascalCaseFiles: true, + } as Schema); + expect( + tree.exists('libs/my-dir/my-lib/src/lib/MyDirMyLib.ts') + ).toBeTruthy(); + expect( + tree.exists('libs/my-dir/my-lib/src/lib/MyDirMyLib.spec.ts') + ).toBeTruthy(); + expect( + tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.ts') + ).toBeFalsy(); + expect( + tree.exists('libs/my-dir/my-lib/src/lib/my-dir-my-lib.spec.ts') + ).toBeFalsy(); + }); + }); +}); diff --git a/packages/node/src/generators/library/library.ts b/packages/node/src/generators/library/library.ts new file mode 100644 index 0000000000..5e987ea36b --- /dev/null +++ b/packages/node/src/generators/library/library.ts @@ -0,0 +1,156 @@ +import { + convertNxGenerator, + formatFiles, + generateFiles, + getWorkspaceLayout, + joinPathFragments, + names, + offsetFromRoot, + readProjectConfiguration, + toJS, + Tree, + updateProjectConfiguration, + updateTsConfigsToJs, +} from '@nrwl/devkit'; +import { getImportPath } from 'nx/src/utils/path'; +import { Schema } from './schema'; +import { libraryGenerator as workspaceLibraryGenerator } from '@nrwl/workspace/generators'; +import { join } from 'path'; + +export interface NormalizedSchema extends Schema { + name: string; + prefix: string; + fileName: string; + projectRoot: string; + projectDirectory: string; + parsedTags: string[]; + compiler: 'swc' | 'tsc'; +} + +export async function libraryGenerator(tree: Tree, schema: Schema) { + const options = normalizeOptions(tree, schema); + + if (options.publishable === true && !schema.importPath) { + throw new Error( + `For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)` + ); + } + + const libraryInstall = await workspaceLibraryGenerator(tree, { + ...schema, + importPath: options.importPath, + testEnvironment: 'node', + skipFormat: true, + setParserOptionsProject: options.setParserOptionsProject, + }); + createFiles(tree, options); + + if (options.js) { + updateTsConfigsToJs(tree, options); + } + updateProject(tree, options); + + if (!schema.skipFormat) { + await formatFiles(tree); + } + + return libraryInstall; +} + +export default libraryGenerator; +export const librarySchematic = convertNxGenerator(libraryGenerator); + +function normalizeOptions(tree: Tree, options: Schema): NormalizedSchema { + const { npmScope, libsDir } = getWorkspaceLayout(tree); + const name = names(options.name).fileName; + const projectDirectory = options.directory + ? `${names(options.directory).fileName}/${name}` + : name; + + const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-'); + const fileName = getCaseAwareFileName({ + fileName: options.simpleModuleName ? name : projectName, + pascalCaseFiles: options.pascalCaseFiles, + }); + const projectRoot = joinPathFragments(libsDir, projectDirectory); + + const parsedTags = options.tags + ? options.tags.split(',').map((s) => s.trim()) + : []; + + const importPath = + options.importPath || getImportPath(npmScope, projectDirectory); + + return { + ...options, + prefix: npmScope, // we could also allow customizing this + fileName, + name: projectName, + projectRoot, + projectDirectory, + parsedTags, + importPath, + }; +} + +function getCaseAwareFileName(options: { + pascalCaseFiles: boolean; + fileName: string; +}) { + const normalized = names(options.fileName); + + return options.pascalCaseFiles ? normalized.className : normalized.fileName; +} + +function createFiles(tree: Tree, options: NormalizedSchema) { + const { className, name, propertyName } = names(options.fileName); + + generateFiles(tree, join(__dirname, './files/lib'), options.projectRoot, { + ...options, + className, + name, + propertyName, + tmpl: '', + offsetFromRoot: offsetFromRoot(options.projectRoot), + }); + + if (options.unitTestRunner === 'none') { + tree.delete( + join(options.projectRoot, `./src/lib/${options.fileName}.spec.ts`) + ); + } + if (!options.publishable && !options.buildable) { + tree.delete(join(options.projectRoot, 'package.json')); + } + if (options.js) { + toJS(tree); + } +} + +function updateProject(tree: Tree, options: NormalizedSchema) { + if (!options.publishable && !options.buildable) { + return; + } + + const project = readProjectConfiguration(tree, options.name); + const { libsDir } = getWorkspaceLayout(tree); + + project.targets = project.targets || {}; + project.targets.build = { + executor: `@nrwl/js:${options.compiler}`, + outputs: ['{options.outputPath}'], + options: { + outputPath: `dist/${libsDir}/${options.projectDirectory}`, + tsConfig: `${options.projectRoot}/tsconfig.lib.json`, + packageJson: `${options.projectRoot}/package.json`, + main: `${options.projectRoot}/src/index` + (options.js ? '.js' : '.ts'), + assets: [`${options.projectRoot}/*.md`], + }, + }; + + if (options.rootDir) { + project.targets.build.options.srcRootForCompilationRoot = options.rootDir; + } + + updateProjectConfiguration(tree, options.name, project); +} diff --git a/packages/node/src/generators/library/schema.d.ts b/packages/node/src/generators/library/schema.d.ts new file mode 100644 index 0000000000..682a1c3a1e --- /dev/null +++ b/packages/node/src/generators/library/schema.d.ts @@ -0,0 +1,24 @@ +import { Linter } from '@nrwl/linter'; + +export interface Schema { + name: string; + directory?: string; + simpleModuleName?: boolean; + skipTsConfig?: boolean; + skipFormat?: boolean; + tags?: string; + unitTestRunner?: 'jest' | 'none'; + linter?: Linter; + buildable?: boolean; + publishable?: boolean; + importPath?: string; + testEnvironment?: 'jsdom' | 'node'; + rootDir?: string; + babelJest?: boolean; + js?: boolean; + pascalCaseFiles?: boolean; + strict?: boolean; + standaloneConfig?: boolean; + setParserOptionsProject?: boolean; + compiler: 'tsc' | 'swc'; +} diff --git a/packages/node/src/generators/library/schema.json b/packages/node/src/generators/library/schema.json new file mode 100644 index 0000000000..d0100170fa --- /dev/null +++ b/packages/node/src/generators/library/schema.json @@ -0,0 +1,122 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "$id": "NxNodeLibrary", + "title": "Create a Node Library for Nx", + "description": "Create a Node Library for an Nx workspace.", + "type": "object", + "examples": [ + { + "command": "nx 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 lib is placed", + "alias": "dir" + }, + "simpleModuleName": { + "description": "Keep the module name simple (when using `--directory`).", + "type": "boolean", + "default": false + }, + "linter": { + "description": "The tool to use for running lint checks.", + "type": "string", + "enum": ["eslint"], + "default": "eslint" + }, + "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.base.json` for development experience." + }, + "publishable": { + "type": "boolean", + "description": "Create a publishable library." + }, + "buildable": { + "type": "boolean", + "default": false, + "description": "Generate a buildable library." + }, + "compiler": { + "type": "string", + "enum": ["tsc", "swc"], + "default": "tsc", + "description": "The compiler used by the build and test targets." + }, + "importPath": { + "type": "string", + "description": "The library name used to import it, like `@myorg/my-awesome-lib`. Must be a valid npm name." + }, + "rootDir": { + "type": "string", + "description": "Sets the `rootDir` for TypeScript compilation. When not defined, it uses the project's root property, or `srcRootForCompilationRoot` if it is defined." + }, + "testEnvironment": { + "type": "string", + "enum": ["jsdom", "node"], + "description": "The test environment to use if `unitTestRunner` is set to `jest`.", + "default": "jsdom" + }, + "babelJest": { + "type": "boolean", + "description": "Use `babel` instead of `ts-jest`.", + "default": false + }, + "pascalCaseFiles": { + "type": "boolean", + "description": "Use pascal case file names.", + "alias": "P", + "default": false + }, + "js": { + "type": "boolean", + "description": "Generate JavaScript files rather than TypeScript files.", + "default": false + }, + "strict": { + "type": "boolean", + "description": "Whether to enable tsconfig strict mode or not.", + "default": false + }, + "standaloneConfig": { + "description": "Split the project configuration into `/project.json` rather than including it inside `workspace.json`.", + "type": "boolean" + }, + "setParserOptionsProject": { + "type": "boolean", + "description": "Whether or not to configure the ESLint `parserOptions.project`. We do not do this by default for lint performance reasons.", + "default": false + } + }, + "required": ["name"] +}