From d570127412ff1190b60b1f96599aa369b7213fb2 Mon Sep 17 00:00:00 2001 From: Juri Date: Fri, 5 Jun 2020 15:39:29 +0200 Subject: [PATCH] fix(angular): adjust generated tsconfig path mapping for publ libs ISSUES CLOSED: #2794 --- .../angular/api-angular/schematics/library.md | 6 ++ docs/angular/api-react/schematics/library.md | 6 ++ docs/react/api-angular/schematics/library.md | 6 ++ package.json | 3 +- .../library/lib/normalize-options.ts | 14 ++- .../lib/update-lib-package-npm-scope.ts | 12 +-- .../schematics/library/lib/update-tsconfig.ts | 17 +++- .../src/schematics/library/library.spec.ts | 92 +++++++++++++++++++ .../angular/src/schematics/library/library.ts | 5 + .../src/schematics/library/schema.d.ts | 1 + .../src/schematics/library/schema.json | 4 + packages/workspace/package.json | 3 +- .../src/utils/validate-npm-pkg-name.spec.ts | 40 ++++++++ .../src/utils/validate-npm-pkg-name.ts | 91 ++++++++++++++++++ yarn.lock | 5 + 15 files changed, 289 insertions(+), 16 deletions(-) create mode 100644 packages/workspace/src/utils/validate-npm-pkg-name.spec.ts create mode 100644 packages/workspace/src/utils/validate-npm-pkg-name.ts diff --git a/docs/angular/api-angular/schematics/library.md b/docs/angular/api-angular/schematics/library.md index 2b5aa0917c4c10..1031e95debbc98 100644 --- a/docs/angular/api-angular/schematics/library.md +++ b/docs/angular/api-angular/schematics/library.md @@ -42,6 +42,12 @@ Type: `string` A directory where the lib is placed +### importPath + +Type: `string` + +The library name used to import it, like @myorg/my-awesome-lib + ### lazy Default: `false` diff --git a/docs/angular/api-react/schematics/library.md b/docs/angular/api-react/schematics/library.md index 17f834610fe2bb..610e27d8b10596 100644 --- a/docs/angular/api-react/schematics/library.md +++ b/docs/angular/api-react/schematics/library.md @@ -76,6 +76,12 @@ Type: `string` A directory where the lib is placed +### importPath + +Type: `string` + +The library name used to import it, like @myorg/my-awesome-lib + ### js Default: `false` diff --git a/docs/react/api-angular/schematics/library.md b/docs/react/api-angular/schematics/library.md index 0e42d88a65d72e..bb524dc6d64883 100644 --- a/docs/react/api-angular/schematics/library.md +++ b/docs/react/api-angular/schematics/library.md @@ -42,6 +42,12 @@ Type: `string` A directory where the lib is placed +### importPath + +Type: `string` + +The library name used to import it, like @myorg/my-awesome-lib + ### lazy Default: `false` diff --git a/package.json b/package.json index ed30cb45b6c3b4..221774ee6e5522 100644 --- a/package.json +++ b/package.json @@ -252,6 +252,7 @@ } }, "dependencies": { - "@nrwl/nx-cloud": "^9.3.2" + "@nrwl/nx-cloud": "^9.3.2", + "speakingurl": "^14.0.1" } } diff --git a/packages/angular/src/schematics/library/lib/normalize-options.ts b/packages/angular/src/schematics/library/lib/normalize-options.ts index 12ad9d797b9fd5..14daf8c2574131 100644 --- a/packages/angular/src/schematics/library/lib/normalize-options.ts +++ b/packages/angular/src/schematics/library/lib/normalize-options.ts @@ -1,6 +1,6 @@ import { Tree } from '@angular-devkit/schematics'; -import { getNpmScope, toClassName, toFileName } from '@nrwl/workspace'; -import { libsDir } from '@nrwl/workspace/src/utils/ast-utils'; +import { getNpmScope, toClassName, toFileName, NxJson } from '@nrwl/workspace'; +import { libsDir, readJsonInTree } from '@nrwl/workspace/src/utils/ast-utils'; import { Schema } from '../schema'; import { NormalizedSchema } from './normalized-schema'; @@ -24,6 +24,15 @@ export function normalizeOptions( const modulePath = `${projectRoot}/src/lib/${fileName}.module.ts`; const defaultPrefix = getNpmScope(host); + // adjust the import path, especially for publishable + // libs which need to respect the NPM package scoping rules + let importPath = options.importPath; + if (!importPath) { + importPath = options.publishable + ? `@${defaultPrefix}/${projectName}` + : `@${defaultPrefix}/${projectDirectory}`; + } + return { ...options, prefix: options.prefix ? options.prefix : defaultPrefix, @@ -35,5 +44,6 @@ export function normalizeOptions( modulePath, parsedTags, fileName, + importPath, }; } diff --git a/packages/angular/src/schematics/library/lib/update-lib-package-npm-scope.ts b/packages/angular/src/schematics/library/lib/update-lib-package-npm-scope.ts index d08195cd1f72b5..9e0c6184b73b03 100644 --- a/packages/angular/src/schematics/library/lib/update-lib-package-npm-scope.ts +++ b/packages/angular/src/schematics/library/lib/update-lib-package-npm-scope.ts @@ -1,12 +1,10 @@ import { Rule, Tree } from '@angular-devkit/schematics'; -import { getNpmScope, updateJsonInTree } from '@nrwl/workspace'; +import { updateJsonInTree } from '@nrwl/workspace'; import { NormalizedSchema } from './normalized-schema'; export function updateLibPackageNpmScope(options: NormalizedSchema): Rule { - return (host: Tree) => { - return updateJsonInTree(`${options.projectRoot}/package.json`, (json) => { - json.name = `@${getNpmScope(host)}/${options.name}`; - return json; - }); - }; + return updateJsonInTree(`${options.projectRoot}/package.json`, (json) => { + json.name = options.importPath; + return json; + }); } diff --git a/packages/angular/src/schematics/library/lib/update-tsconfig.ts b/packages/angular/src/schematics/library/lib/update-tsconfig.ts index 01b4687de21af6..9fcfff27680015 100644 --- a/packages/angular/src/schematics/library/lib/update-tsconfig.ts +++ b/packages/angular/src/schematics/library/lib/update-tsconfig.ts @@ -2,23 +2,30 @@ import { chain, Rule, SchematicContext, + SchematicsException, Tree, } from '@angular-devkit/schematics'; -import { NxJson, readJsonInTree, updateJsonInTree } from '@nrwl/workspace'; -import { libsDir } from '@nrwl/workspace/src/utils/ast-utils'; +import { updateJsonInTree } from '@nrwl/workspace'; import { NormalizedSchema } from './normalized-schema'; export function updateTsConfig(options: NormalizedSchema): Rule { return chain([ (host: Tree, context: SchematicContext) => { - const nxJson = readJsonInTree(host, 'nx.json'); return updateJsonInTree('tsconfig.json', (json) => { const c = json.compilerOptions; c.paths = c.paths || {}; delete c.paths[options.name]; - c.paths[`@${nxJson.npmScope}/${options.projectDirectory}`] = [ - `${libsDir(host)}/${options.projectDirectory}/src/index.ts`, + + if (c.paths[options.importPath]) { + throw new SchematicsException( + `You already have a library using the import path "${options.importPath}". Make sure to specify a unique one.` + ); + } + + c.paths[options.importPath] = [ + `libs/${options.projectDirectory}/src/index.ts`, ]; + return json; })(host, context); }, diff --git a/packages/angular/src/schematics/library/library.spec.ts b/packages/angular/src/schematics/library/library.spec.ts index 71499a94278931..1bb0520bfff553 100644 --- a/packages/angular/src/schematics/library/library.spec.ts +++ b/packages/angular/src/schematics/library/library.spec.ts @@ -470,6 +470,21 @@ describe('lib', () => { ).toBeUndefined(); }); + it('should use an npm friendly name in tsconfig.json when publishable', async () => { + const tree = await runSchematic( + 'lib', + { name: 'myLib', directory: 'myDir', publishable: true }, + 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 update tsconfig.json (no existing path mappings)', async () => { const updatedTree: any = updateJsonInTree('tsconfig.json', (json) => { json.compilerOptions.paths = undefined; @@ -1007,4 +1022,81 @@ describe('lib', () => { ).toEqual(['libs/my-lib/tsconfig.lib.json']); }); }); + + describe('--importPath', () => { + it('should update the package.json & tsconfig with the given import path', async () => { + const tree = await runSchematic( + 'lib', + { + name: 'myLib', + framework: 'angular', + publishable: true, + directory: 'myDir', + importPath: '@myorg/lib', + }, + appTree + ); + const packageJson = readJsonInTree( + tree, + 'libs/my-dir/my-lib/package.json' + ); + const tsconfigJson = readJsonInTree(tree, '/tsconfig.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 () => { + const tree1 = await runSchematic( + 'lib', + { + name: 'myLib1', + framework: 'angular', + publishable: true, + importPath: '@myorg/lib', + }, + appTree + ); + + try { + await runSchematic( + 'lib', + { + name: 'myLib2', + framework: 'angular', + publishable: true, + importPath: '@myorg/lib', + }, + tree1 + ); + } catch (e) { + expect(e.message).toContain( + 'You already have a library using the import path' + ); + } + + expect.assertions(1); + }); + + it('should fail if we pass an invalid npm package name', async () => { + try { + await runSchematic( + 'lib', + { + name: 'myLib', + framework: 'angular', + publishable: true, + importPath: '@myorg/shop/mylib', + }, + appTree + ); + } catch (e) { + expect(e.message).toContain('scoped package name has an extra'); + } + + expect.assertions(1); + }); + }); }); diff --git a/packages/angular/src/schematics/library/library.ts b/packages/angular/src/schematics/library/library.ts index 26bf005e5e9b21..b29b262a30d88a 100644 --- a/packages/angular/src/schematics/library/library.ts +++ b/packages/angular/src/schematics/library/library.ts @@ -15,6 +15,7 @@ import { updateLibPackageNpmScope } from './lib/update-lib-package-npm-scope'; import { updateProject } from './lib/update-project'; import { updateTsConfig } from './lib/update-tsconfig'; import { Schema } from './schema'; +import { validateNpmPackageName } from '@nrwl/workspace/src/utils/validate-npm-pkg-name'; export default function (schema: Schema): Rule { return (host: Tree): Rule => { @@ -23,6 +24,10 @@ export default function (schema: Schema): Rule { throw new Error(`routing must be set`); } + if (options.publishable === true) { + validateNpmPackageName(options.importPath); + } + return chain([ addLintFiles(options.projectRoot, Linter.TsLint, { onlyGlobal: true }), addUnitTestRunner(options), diff --git a/packages/angular/src/schematics/library/schema.d.ts b/packages/angular/src/schematics/library/schema.d.ts index 11b939b4dd35c5..1f1ebaf8920fd9 100644 --- a/packages/angular/src/schematics/library/schema.d.ts +++ b/packages/angular/src/schematics/library/schema.d.ts @@ -8,6 +8,7 @@ export interface Schema { directory?: string; sourceDir?: string; publishable: boolean; + importPath?: string; spec?: boolean; flat?: boolean; diff --git a/packages/angular/src/schematics/library/schema.json b/packages/angular/src/schematics/library/schema.json index 6140a94f5204e7..78861b24355ecc 100644 --- a/packages/angular/src/schematics/library/schema.json +++ b/packages/angular/src/schematics/library/schema.json @@ -104,6 +104,10 @@ "enum": ["karma", "jest", "none"], "description": "Test runner to use for unit tests", "default": "jest" + }, + "importPath": { + "type": "string", + "description": "The library name used to import it, like @myorg/my-awesome-lib" } }, "required": [] diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 77f0b866406386..60304b50052a5c 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -67,6 +67,7 @@ "yargs": "^11.0.0", "chalk": "2.4.2", "@nrwl/cli": "*", - "axios": "0.19.2" + "axios": "0.19.2", + "speakingurl": "14.0.1" } } diff --git a/packages/workspace/src/utils/validate-npm-pkg-name.spec.ts b/packages/workspace/src/utils/validate-npm-pkg-name.spec.ts new file mode 100644 index 00000000000000..ae4bb2678120ec --- /dev/null +++ b/packages/workspace/src/utils/validate-npm-pkg-name.spec.ts @@ -0,0 +1,40 @@ +import { validateNpmPackageName } from './validate-npm-pkg-name'; + +/** + * Rules: https://docs.npmjs.com/files/package.json#name + */ + +describe('Validate npm package name', () => { + ['a'.repeat(214), '@myorg/awesomeness', 'awesomelib'].forEach((pkgName) => { + it(`should succeed for ${pkgName}`, () => { + expect(validateNpmPackageName(pkgName)).toBeTruthy(); + }); + }); + + [ + { + name: 'a'.repeat(215), + error: /more than 214 characters/, + }, + { name: '_myawesomepkg', error: /cannot start with a dot nor underscore/ }, + { name: '.myawesomepkg', error: /cannot start with a dot nor underscore/ }, + { name: 'Mypackage', error: /uppercase letters/ }, + { name: '@my/super/org', error: /scoped package name has an extra/ }, + { + name: '@my/SuperPkg', + error: /package name cannot have uppercase letters/, + }, + ].forEach((pkgTest) => { + it(`should fail for ${pkgTest.name}`, () => { + function execValidation() { + validateNpmPackageName(pkgTest.name); + } + + if (pkgTest.error) { + expect(execValidation).toThrowError(pkgTest.error); + } else { + expect(execValidation).toThrowError(); + } + }); + }); +}); diff --git a/packages/workspace/src/utils/validate-npm-pkg-name.ts b/packages/workspace/src/utils/validate-npm-pkg-name.ts new file mode 100644 index 00000000000000..034ea5a59cb1c0 --- /dev/null +++ b/packages/workspace/src/utils/validate-npm-pkg-name.ts @@ -0,0 +1,91 @@ +/** + * ADAPTED FROM: https://github.com/lassjs/is-valid-npm-name + */ + +import * as slug from 'speakingurl'; + +const errors = { + notString: 'package name must be a String', + trim: 'remove trailing spaces from start and end of package name', + maxLength: 'package name cannot be more than 214 characters', + dotUnderscore: 'package name cannot start with a dot nor underscore', + uppercase: 'package name cannot have uppercase letters', + atFirst: 'scoped package name must start with "@" character', + extraAt: `scoped package name has an extra "@" character`, + noSlash: 'scoped package name must be in the format of @myorg/package', + extraSlash: `scoped package name has an extra "/" character`, + builtIn: 'package name cannot use built-in core Node module name', + nonURLSafe: 'package name had non-URL-safe characters', +}; + +function isValidNpmPackageName(str: string) { + // ensure it's a string + if (!(typeof str === 'string' && str !== '')) return errors.notString; + + // first trim it + if (str !== str.trim()) return errors.trim; + + // can't be > 214 characters + if (str.length > 214) return errors.maxLength; + + // can't start with a dot or underscore + if (['.', '_'].includes(str.slice(0, 1))) return errors.dotUnderscore; + + // no uppercase letters + if (str !== str.toLowerCase()) return errors.uppercase; + + // + // name can be prefixed by a scope, e.g. @myorg/package + // + + // must have @ + if (str.includes('@')) { + // must have @ at beginning of string + if (str.indexOf('@') !== 0) return errors.atFirst; + + // must have only one @ + if (str.indexOf('@') !== str.lastIndexOf('@')) return errors.extraAt; + + // must have / + if (!str.includes('/')) return errors.noSlash; + + // must have only one / + if (str.indexOf('/') !== str.lastIndexOf('/')) return errors.extraSlash; + + // validate scope + const arr = str.split('/'); + const scope = arr[0].slice(1); + const isValidScopeName = isValidNpmPackageName(scope); + + if (isValidScopeName !== true) return isValidScopeName; + + // validate name again + return isValidNpmPackageName(arr[1]); + } + + // // don't use the same name as a core Node module + // // + // if (_builtinLibs.includes(str)) return errors.builtIn; + + // no non-URL-safe characters + // + const safeStr = slug(str); + if (str !== safeStr) + return `${errors.nonURLSafe}, try using "${safeStr}" instead`; + + return true; +} + +/** + * Throws an error if the validation fails + * @param name name of the package + */ +export function validateNpmPackageName(str: string) { + const result = isValidNpmPackageName(str); + + if (result !== true) { + throw new Error(result); + } else { + return true; + } +} diff --git a/yarn.lock b/yarn.lock index 7ec24bc004584f..d025858afdf5e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19485,6 +19485,11 @@ spdy@^4.0.1, spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" +speakingurl@^14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53" + integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ== + speed-measure-webpack-plugin@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.3.1.tgz#69840a5cdc08b4638697dac7db037f595d7f36a0"