From cbd9b675f1d4b88bb6c071a9bf848ae8ecafadb1 Mon Sep 17 00:00:00 2001 From: Jessica Gomez Date: Wed, 20 Dec 2023 20:25:30 +0100 Subject: [PATCH] feat: Schematic for extracting libraries from projects JIRA-Ticket: AV-27910 --- README.md | 17 +- package-lock.json | 4 +- package.json | 2 +- src/collection.json | 5 + .../files/.eslintrc.json.template | 41 +++++ .../files/jest.config.ts.template | 31 ++++ .../files/tsconfig-linter.json.template | 15 ++ .../files/tsconfig.json.template | 19 +++ .../files/tsconfig.lib.json.template | 19 +++ .../files/tsconfig.spec.json.template | 17 ++ src/extract-app-libs/index.js | 108 +++++++++++++ src/extract-app-libs/index.ts | 149 ++++++++++++++++++ src/extract-app-libs/schema.json | 57 +++++++ 13 files changed, 480 insertions(+), 4 deletions(-) create mode 100644 src/extract-app-libs/files/.eslintrc.json.template create mode 100644 src/extract-app-libs/files/jest.config.ts.template create mode 100644 src/extract-app-libs/files/tsconfig-linter.json.template create mode 100644 src/extract-app-libs/files/tsconfig.json.template create mode 100644 src/extract-app-libs/files/tsconfig.lib.json.template create mode 100644 src/extract-app-libs/files/tsconfig.spec.json.template create mode 100644 src/extract-app-libs/index.js create mode 100644 src/extract-app-libs/index.ts create mode 100644 src/extract-app-libs/schema.json diff --git a/README.md b/README.md index 7671778..a232898 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,21 @@ ## Generation schematics Generation schematics are instructions for the ``ng generate`` command. Avrios' Schematics [support custom API Services, components, modals, page components, ngrx store and library generation](https://github.com/avrios/angular-schematics/blob/master/src/collection.json). - + Usage example: ``ng generate avrios-schematics:component `` + +## Schematic: library (deprecated) + +## Schematic: extract-app-libs +Generates libraries for a specific Avrio's app scope. + +### inputs: +- `**name:**` The name of the project this libraries belong to. If none given then the libraries are added to the the shared scope. +- `**namespace:**` The namespace of the Library. Normally matches the project name. Ex: `@secure, @shared``. +- `**tags:**` The list of tags the libraries are allowed to use. +- `**prefix:**` The prefix to apply to the generated libraries. + +For this schematic to work you have to: +- extract the apps folders that you want to convert to libraries to the path libs//src/lib +- run `npm run ng generate avrios-schematics:extract-app-libs ` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 696cf6b..94ac5bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "avrios-schematics", - "version": "2.4.1", + "version": "2.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "avrios-schematics", - "version": "2.4.1", + "version": "2.6.0", "license": "MIT", "devDependencies": { "@angular-devkit/architect": "0.1300.1", diff --git a/package.json b/package.json index dfbcd21..795fb11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "avrios-schematics", - "version": "2.5.0", + "version": "2.6.0", "description": "A blank schematics", "scripts": { "build": "tsc -p tsconfig.json" diff --git a/src/collection.json b/src/collection.json index bd0b2a4..1fe868b 100644 --- a/src/collection.json +++ b/src/collection.json @@ -30,6 +30,11 @@ "description": "Create an Avrios library", "factory": "./library", "schema": "./library/schema.json" + }, + "extract-app-libs": { + "description": "Extract app libs from an Avrios project", + "factory": "./extract-app-libs", + "schema": "./extract-app-libs/schema.json" } } } diff --git a/src/extract-app-libs/files/.eslintrc.json.template b/src/extract-app-libs/files/.eslintrc.json.template new file mode 100644 index 0000000..875a3bd --- /dev/null +++ b/src/extract-app-libs/files/.eslintrc.json.template @@ -0,0 +1,41 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*", "**/*.stories.ts"], + "overrides": [ + { + "files": ["*.ts"], + "extends": ["plugin:@nrwl/nx/angular", "plugin:@angular-eslint/template/process-inline-templates"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "avr", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "avr", + "style": "kebab-case" + } + ], + "@angular-eslint/no-output-on-prefix": "off", + "@typescript-eslint/no-namespace": "off" + }, + "plugins": ["@typescript-eslint"], + "parserOptions": { + "project": "libs/<%= dasherize(name) %>/tsconfig-linter.json" + } + }, { + "files": ["*.html"], + "extends": ["plugin:@nrwl/nx/angular-template"], + "rules": { + "@angular-eslint/template/no-negated-async": "off", + "@angular-eslint/template/eqeqeq": "warn" + } + } + ] +} \ No newline at end of file diff --git a/src/extract-app-libs/files/jest.config.ts.template b/src/extract-app-libs/files/jest.config.ts.template new file mode 100644 index 0000000..df90a3d --- /dev/null +++ b/src/extract-app-libs/files/jest.config.ts.template @@ -0,0 +1,31 @@ +const esModules = ['design-system-toolkit', '.*\\.mjs$'].join('|'); + +module.exports = { + displayName: '<%= dasherize(name) %>', + preset: '../../jest.config.ts', + coverageDirectory: '../../coverage/libs/<%= dasherize(name) %>', + setupFilesAfterEnv: ['../../setupJest.ts'], + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + }, + transform: { + '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', + [`(${esModules}).+\\.js$`]: 'babel-jest', + }, + transformIgnorePatterns: [`node_modules/(?!${esModules})`], + + + moduleNameMapper: { + '^@shared(.*)$': '../shared/src/lib$1', + '^@<%= dasherize(name) %>(.*)$': '/src/lib$1' + }, + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], + testRunner: 'jest-jasmine2' +}; diff --git a/src/extract-app-libs/files/tsconfig-linter.json.template b/src/extract-app-libs/files/tsconfig-linter.json.template new file mode 100644 index 0000000..34f57e7 --- /dev/null +++ b/src/extract-app-libs/files/tsconfig-linter.json.template @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "./**/*.ts", + "src/**/*.ts" + ], + "exclude": [ + "../../dist/**/*", + "../../release/**/*", + "../../node_modules/**/*", + "../../tmp/**/*", + "**/*.stories.ts", + "**/*.stories.js" + ] +} diff --git a/src/extract-app-libs/files/tsconfig.json.template b/src/extract-app-libs/files/tsconfig.json.template new file mode 100644 index 0000000..d974861 --- /dev/null +++ b/src/extract-app-libs/files/tsconfig.json.template @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.json", + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "paths": { + "@shared/*": ["libs/shared/src/lib/*"], + "@<%= dasherize(namespace) %>/*": ["libs/<%= dasherize(name) %>/src/lib/*"] + }, + "target": "es2020" + } +} diff --git a/src/extract-app-libs/files/tsconfig.lib.json.template b/src/extract-app-libs/files/tsconfig.lib.json.template new file mode 100644 index 0000000..044f411 --- /dev/null +++ b/src/extract-app-libs/files/tsconfig.lib.json.template @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "angular-loading-bar", + "angular-promise-tracker", + "angular-translate", + "ng-file-upload", + "node" + ] + }, + "exclude": [ + "**/*.spec.ts" + ], + "include": [ + "**/*.ts" + ] +} diff --git a/src/extract-app-libs/files/tsconfig.spec.json.template b/src/extract-app-libs/files/tsconfig.spec.json.template new file mode 100644 index 0000000..0ef8079 --- /dev/null +++ b/src/extract-app-libs/files/tsconfig.spec.json.template @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": [ + "angular-ui-bootstrap", + "jest", + "node" + ] + }, + "include": [ + "../../libs/**/*.d.ts", + "**/*.spec.ts", + "**/*.test.ts" + ] +} diff --git a/src/extract-app-libs/index.js b/src/extract-app-libs/index.js new file mode 100644 index 0000000..e48da8c --- /dev/null +++ b/src/extract-app-libs/index.js @@ -0,0 +1,108 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const core_1 = require("@angular-devkit/core"); +const schematics_1 = require("@angular-devkit/schematics"); +const validation_1 = require("../utilities/validation"); +const parse_name_1 = require("../utilities/parse-name"); +function default_1(options) { + return (host) => { + const projectJsonFile = '/project.json'; + const { prefix, name, tags: tagsString } = options; + const dasherizedName = core_1.strings.dasherize(name); + const projectLibsRoot = `libs/${dasherizedName}`; + const parsedPath = (0, parse_name_1.parseName)(projectLibsRoot, name); + (0, validation_1.validateName)(parsedPath.name); + const appScope = `scope:${dasherizedName}`; + const namespace = options.namespace ? options.namespace : parsedPath.name; + const paths = {}; + const libraryNames = host.getDir(`${projectLibsRoot}/src/lib`).subdirs; + libraryNames.forEach(libraryName => { + (0, validation_1.validateName)(libraryName); + const sourceRoot = `${projectLibsRoot}/src/lib/${libraryName}`; + paths[`@${namespace}/${libraryName}/*`] = [`${projectLibsRoot}/src/lib/${libraryName}/*`]; + const projectConfig = { + name: `${dasherizedName}-${libraryName}`, + sourceRoot, + prefix, + tags: [appScope], + projectType: 'library', + generators: {}, + targets: { + lint: { + executor: '@nrwl/linter:eslint', + options: { + lintFilePatterns: [ + `${sourceRoot}/**/*.ts`, + `${sourceRoot}/**/*.html` + ], + eslintConfig: `${projectLibsRoot}/.eslintrc.json` + } + }, + test: { + executor: '@nrwl/jest:jest', + options: { + jestConfig: `${projectLibsRoot}/jest.config.ts`, + passWithNoTests: true, + testPathPattern: [`lib/${libraryName}/`] + } + } + } + }; + host.exists(`${sourceRoot}/${projectJsonFile}`) || host.create(`${sourceRoot}/${projectJsonFile}`, JSON.stringify(projectConfig)); + }); + const appTsConfigPath = `apps/${dasherizedName}/tsconfig.json`; + const appEslintrcPath = `apps/${dasherizedName}/.eslintrc.json`; + const eslintrcPath = `/.eslintrc.json`; + const appTsConfig = host.read(appTsConfigPath); + const appEslintrc = host.read(appEslintrcPath); + const eslintrc = host.read(eslintrcPath); + if (!appTsConfig) { + throw new schematics_1.SchematicsException(`Could not find ${dasherizedName}/tsConfig.json file.`); + } + if (!appEslintrc) { + throw new schematics_1.SchematicsException(`Could not find ${dasherizedName}/.eslintrc.json file.`); + } + if (!eslintrc) { + throw new schematics_1.SchematicsException(`Could not find /.eslintrc.json file.`); + } + // Add new libs paths to tsconfig.json + const appTsConfigJson = JSON.parse(appTsConfig.toString()); + appTsConfigJson.compilerOptions.paths = Object.assign(Object.assign({}, paths), appTsConfigJson.compilerOptions.paths); + // Remove deactivated eslint rule @nrwl/nx/enforce-module-boundaries rule from app's .eslintrc.json + const appEslintrcJson = JSON.parse(appEslintrc.toString()); + appEslintrcJson.overrides.forEach((elem) => { + if (elem.rules && elem.rules['@nrwl/nx/enforce-module-boundaries'] === 'off') { + delete elem.rules['@nrwl/nx/enforce-module-boundaries']; + } + }); + const eslintrcJson = JSON.parse(eslintrc.toString()); + const tags = tagsString ? tagsString.split(',').map(s => s.trim()) : []; + eslintrcJson.overrides.forEach((elem) => { + if (elem.rules && elem.rules['@nrwl/nx/enforce-module-boundaries']) { + const found = elem.rules['@nrwl/nx/enforce-module-boundaries'][1].depConstraints.filter((constraint) => constraint.sourceTag === appScope); + if (!found || !found.length) { + elem.rules['@nrwl/nx/enforce-module-boundaries'][1].depConstraints.push({ + sourceTag: appScope, + onlyDependOnLibsWithTags: [...tags, appScope] + }); + } + } + }); + host.overwrite(appTsConfigPath, JSON.stringify(appTsConfigJson)); + host.overwrite(appEslintrcPath, JSON.stringify(appEslintrcJson)); + host.overwrite(eslintrcPath, JSON.stringify(eslintrcJson)); + options.name = parsedPath.name; + options.path = parsedPath.path; + options.namespace = namespace; + const templateSource = (0, schematics_1.apply)((0, schematics_1.url)('./files'), [ + (0, schematics_1.applyTemplates)(Object.assign(Object.assign({}, core_1.strings), options)), + (0, schematics_1.move)(projectLibsRoot), + ]); + return (0, schematics_1.chain)([ + _ => host, + (0, schematics_1.mergeWith)(templateSource) + ]); + }; +} +exports.default = default_1; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/src/extract-app-libs/index.ts b/src/extract-app-libs/index.ts new file mode 100644 index 0000000..1406a12 --- /dev/null +++ b/src/extract-app-libs/index.ts @@ -0,0 +1,149 @@ +import { strings } from '@angular-devkit/core'; +import { + Rule, + SchematicsException, + Tree, + apply, + applyTemplates, + chain, + mergeWith, + move, + url +} from '@angular-devkit/schematics'; + +import { validateName } from '../utilities/validation'; +import { parseName } from '../utilities/parse-name'; + +interface LibraryOptions { + name: string; + prefix: string; + tags?: string; + namespace?: string; + path?: string; +} + +export default function (options: LibraryOptions): Rule { + return (host: Tree) => { + const projectJsonFile = '/project.json'; + const { prefix, name, tags: tagsString } = options; + const dasherizedName = strings.dasherize(name); + + const projectLibsRoot = `libs/${dasherizedName}`; + + const parsedPath = parseName(projectLibsRoot, name); + validateName(parsedPath.name); + + const appScope = `scope:${dasherizedName}`; + const namespace = options.namespace ? options.namespace : parsedPath.name; + const paths: Record= {}; + + const libraryNames = host.getDir(`${projectLibsRoot}/src/lib`).subdirs; + libraryNames.forEach(libraryName => { + validateName(libraryName); + const sourceRoot = `${projectLibsRoot}/src/lib/${libraryName}`; + paths[`@${namespace}/${libraryName}/*`] = [`${projectLibsRoot}/src/lib/${libraryName}/*`]; + + const projectConfig = { + name: `${dasherizedName}-${libraryName}`, + sourceRoot, + prefix, + tags: [appScope], + projectType: 'library', + generators: {}, + targets: { + lint: { + executor: '@nrwl/linter:eslint', + options: { + lintFilePatterns: [ + `${sourceRoot}/**/*.ts`, + `${sourceRoot}/**/*.html` + ], + eslintConfig: `${projectLibsRoot}/.eslintrc.json` + } + }, + test: { + executor: '@nrwl/jest:jest', + options: { + jestConfig: `${projectLibsRoot}/jest.config.ts`, + passWithNoTests: true, + testPathPattern: [`lib/${libraryName}/`] + } + } + } + }; + + host.exists(`${sourceRoot}/${projectJsonFile}`) || host.create(`${sourceRoot}/${projectJsonFile}`, JSON.stringify(projectConfig)); + }); + + const appTsConfigPath = `apps/${dasherizedName}/tsconfig.json`; + const appEslintrcPath = `apps/${dasherizedName}/.eslintrc.json`; + const eslintrcPath = `/.eslintrc.json`; + + const appTsConfig = host.read(appTsConfigPath); + const appEslintrc = host.read(appEslintrcPath); + const eslintrc = host.read(eslintrcPath); + + if (!appTsConfig) { + throw new SchematicsException(`Could not find ${dasherizedName}/tsConfig.json file.`); + } + + if (!appEslintrc) { + throw new SchematicsException(`Could not find ${dasherizedName}/.eslintrc.json file.`); + } + + if (!eslintrc) { + throw new SchematicsException(`Could not find /.eslintrc.json file.`); + } + + // Add new libs paths to tsconfig.json + const appTsConfigJson = JSON.parse(appTsConfig.toString()); + appTsConfigJson.compilerOptions.paths = { + ...paths, + ...appTsConfigJson.compilerOptions.paths + }; + + // Remove deactivated eslint rule @nrwl/nx/enforce-module-boundaries rule from app's .eslintrc.json + const appEslintrcJson = JSON.parse(appEslintrc.toString()); + appEslintrcJson.overrides.forEach((elem: any) => { + if (elem.rules && elem.rules['@nrwl/nx/enforce-module-boundaries'] === 'off') { + delete elem.rules['@nrwl/nx/enforce-module-boundaries']; + } + }); + + const eslintrcJson = JSON.parse(eslintrc.toString()); + const tags = tagsString ? tagsString.split(',').map(s => s.trim()) : []; + eslintrcJson.overrides.forEach((elem: any) => { + if (elem.rules && elem.rules['@nrwl/nx/enforce-module-boundaries']) { + const found = elem.rules['@nrwl/nx/enforce-module-boundaries'][1].depConstraints.filter((constraint: any) => constraint.sourceTag === appScope); + if (!found || !found.length) { + elem.rules['@nrwl/nx/enforce-module-boundaries'][1].depConstraints.push( + { + sourceTag: appScope, + onlyDependOnLibsWithTags: [...tags, appScope] + } + ); + } + } + }); + + host.overwrite(appTsConfigPath, JSON.stringify(appTsConfigJson)); + host.overwrite(appEslintrcPath, JSON.stringify(appEslintrcJson)); + host.overwrite(eslintrcPath, JSON.stringify(eslintrcJson)); + + options.name = parsedPath.name; + options.path = parsedPath.path; + options.namespace = namespace; + + const templateSource = apply( + url('./files'), [ + applyTemplates({ ...strings, ...options }), + move(projectLibsRoot), + + ]); + + return chain([ + _ => host, + mergeWith(templateSource) + ]); + }; +} diff --git a/src/extract-app-libs/schema.json b/src/extract-app-libs/schema.json new file mode 100644 index 0000000..f3a6fa3 --- /dev/null +++ b/src/extract-app-libs/schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "SchematicsAngularComponent", + "title": "Angular Extract libraries", + "type": "object", + "description": "Generates libraries for a specific Avrio's app scope.", + "properties": { + "name": { + "type": "string", + "description": "The name of the project this libraries belong to. If none given then the libraries are added to the the shared scope.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": { + "message": "What is the name of the project you want to Extract?", + "type": "string" + } + }, + "namespace": { + "type": "string", + "description": "The namespace of the Library. Normally matches the project name.", + "$default": { + "$source": "argv", + "index": 1 + }, + "x-prompt": "What namespace would you like to use for the Libraries?" + }, + "tags": { + "type": "string", + "description": "The list of tags the libraries are allowed to use.", + "$default": { + "$source": "argv", + "index": 2 + }, + "x-prompt": "Enter the set of tags that determine the boundaries of the libraries, delimited by comma. You can check the available list of tags in .eslintrc file. Ex: scope:shared." + }, + "prefix": { + "type": "string", + "description": "The prefix to apply to the generated libraries.", + "alias": "p", + "default": "avr", + "oneOf": [ + { + "maxLength": 0 + }, + { + "minLength": 1, + "format": "html-selector" + } + ] + } + }, + "required": [ + "name" + ] + }