diff --git a/npm/cypress-schematic/README.md b/npm/cypress-schematic/README.md index 2b87a8322483..71be26beeaa1 100644 --- a/npm/cypress-schematic/README.md +++ b/npm/cypress-schematic/README.md @@ -31,7 +31,7 @@ ## Requirements -- Angular 13+ +- Angular 14+ ## Usage ⏯ @@ -49,6 +49,8 @@ To install the schematic via cli arguments (installs both e2e and component test ng add @cypress/schematic --e2e --component ``` +The installation will add this schematic to the [default schematic collections](https://angular.io/guide/workspace-config#angular-cli-configuration-options). This allows you to execute the CLI commands without prefixing them with the package name. + To run Cypress in `open` mode within your project: ```shell script @@ -78,37 +80,43 @@ ng run {project-name}:ct To generate a new e2e spec file: ```shell script -ng generate @cypress/schematic:spec +ng generate spec ``` or (without cli prompt) ```shell script -ng generate @cypress/schematic:spec {name} +ng generate spec {name} ``` To generate a new component spec file: ```shell script -ng generate @cypress/schematic:spec --component +ng generate spec --component ``` or (without cli prompt) ```shell script -ng generate @cypress/schematic:spec {component name} --component +ng generate spec {component name} --component ``` To generate a new component spec file in a specific folder: ```shell script -ng generate @cypress/schematic:spec {component name} --component --path {path relative to project root} +ng generate spec {component name} --component --path {path relative to project root} ``` To generate new component spec files alongside all component files in a project: ```shell script -ng generate @cypress/schematic:specs-ct +ng generate specs-ct +``` + +To generate a new, generic component definition with a component spec file in the given or default project. This wraps the [Angular CLI Component Generator](https://angular.io/cli/generate#component) and supports the same arguments. + +```shell script +ng generate component {component name} ``` ## Builder Options 🛠 diff --git a/npm/cypress-schematic/package.json b/npm/cypress-schematic/package.json index eb63ce1f7e9d..bc5e98c58956 100644 --- a/npm/cypress-schematic/package.json +++ b/npm/cypress-schematic/package.json @@ -10,16 +10,16 @@ "lint": "eslint --ext .ts,.json, ." }, "dependencies": { - "@angular-devkit/architect": "^0.1402.1", - "@angular-devkit/core": "^14.2.1", - "@angular-devkit/schematics": "^14.2.1", - "@schematics/angular": "^14.2.1", "jsonc-parser": "^3.0.0", "rxjs": "~6.6.0" }, "devDependencies": { + "@angular-devkit/architect": "^0.1402.1", + "@angular-devkit/core": "^14.2.1", + "@angular-devkit/schematics": "^14.2.1", "@angular-devkit/schematics-cli": "^14.2.1", "@angular/cli": "^14.2.1", + "@schematics/angular": "^14.2.1", "@types/chai-enzyme": "0.6.7", "@types/mocha": "8.0.3", "@types/node": "^18.0.6", @@ -28,8 +28,8 @@ "typescript": "^4.7.4" }, "peerDependencies": { - "@angular/cli": ">=12", - "@angular/core": ">=12" + "@angular/cli": ">=14", + "@angular/core": ">=14" }, "license": "MIT", "repository": { diff --git a/npm/cypress-schematic/src/ct.spec.ts b/npm/cypress-schematic/src/ct.spec.ts index 6eb58f30f9f5..0e1929dd3be4 100644 --- a/npm/cypress-schematic/src/ct.spec.ts +++ b/npm/cypress-schematic/src/ct.spec.ts @@ -35,7 +35,7 @@ const copyAngularMount = async (projectPath: string) => { const cypressSchematicPackagePath = path.join(__dirname, '..') -const ANGULAR_PROJECTS: ProjectFixtureDir[] = ['angular-13', 'angular-14'] +const ANGULAR_PROJECTS: ProjectFixtureDir[] = ['angular-14', 'angular-15'] describe('ng add @cypress/schematic / e2e and ct', function () { this.timeout(1000 * 60 * 5) @@ -49,5 +49,15 @@ describe('ng add @cypress/schematic / e2e and ct', function () { await copyAngularMount(projectPath) await runCommandInProject('yarn ng run angular:ct --watch false --spec src/app/app.component.cy.ts', projectPath) }) + + it('should generate component alongside component spec', async () => { + const projectPath = await scaffoldAngularProject(project) + + await runCommandInProject(`yarn add @cypress/schematic@file:${cypressSchematicPackagePath}`, projectPath) + await runCommandInProject('yarn ng add @cypress/schematic --e2e --component', projectPath) + await copyAngularMount(projectPath) + await runCommandInProject('yarn ng generate c foo', projectPath) + await runCommandInProject('yarn ng run angular:ct --watch false --spec src/app/foo/foo.component.cy.ts', projectPath) + }) } }) diff --git a/npm/cypress-schematic/src/e2e.spec.ts b/npm/cypress-schematic/src/e2e.spec.ts index e65ce18c5781..bd231bc8ea7f 100644 --- a/npm/cypress-schematic/src/e2e.spec.ts +++ b/npm/cypress-schematic/src/e2e.spec.ts @@ -24,7 +24,7 @@ const runCommandInProject = (command: string, projectPath: string) => { const cypressSchematicPackagePath = path.join(__dirname, '..') -const ANGULAR_PROJECTS: ProjectFixtureDir[] = ['angular-13', 'angular-14'] +const ANGULAR_PROJECTS: ProjectFixtureDir[] = ['angular-14', 'angular-15'] describe('ng add @cypress/schematic / only e2e', function () { this.timeout(1000 * 60 * 5) diff --git a/npm/cypress-schematic/src/schematics/collection.json b/npm/cypress-schematic/src/schematics/collection.json index 8141b0ec54a3..b5f9358df36f 100644 --- a/npm/cypress-schematic/src/schematics/collection.json +++ b/npm/cypress-schematic/src/schematics/collection.json @@ -15,6 +15,14 @@ "description": "Create spec files for all Angular components in a project", "factory": "./ng-generate/cypress-ct-tests/index", "schema": "./ng-generate/cypress-ct-tests/schema.json" + }, + "component": { + "description": "Creates a new, generic component definition in the given or default project.", + "aliases": [ + "c" + ], + "factory": "./ng-generate/component/index", + "schema": "./ng-generate/component/schema.json" } } } diff --git a/npm/cypress-schematic/src/schematics/ng-add/index.spec.ts b/npm/cypress-schematic/src/schematics/ng-add/index.spec.ts index a83745411981..bef3bf33c0e1 100644 --- a/npm/cypress-schematic/src/schematics/ng-add/index.spec.ts +++ b/npm/cypress-schematic/src/schematics/ng-add/index.spec.ts @@ -3,6 +3,7 @@ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing' import { join } from 'path' import { expect } from 'chai' +import { JsonObject } from '@angular-devkit/core' describe('@cypress/schematic: ng-add', () => { const schematicRunner = new SchematicTestRunner( @@ -16,6 +17,7 @@ describe('@cypress/schematic: ng-add', () => { name: 'workspace', newProjectRoot: 'projects', version: '6.0.0', + packageManager: 'yarn', } const appOptions = { @@ -26,6 +28,10 @@ describe('@cypress/schematic: ng-add', () => { skipPackageJson: false, } + const readAngularJson = (tree: UnitTestTree) => { + return tree.readJson('/angular.json') as JsonObject + } + beforeEach(async () => { appTree = await schematicRunner.runExternalSchematicAsync('@schematics/angular', 'workspace', workspaceOptions).toPromise() appTree = await schematicRunner.runExternalSchematicAsync('@schematics/angular', 'application', appOptions, appTree).toPromise() @@ -58,4 +64,35 @@ describe('@cypress/schematic: ng-add', () => { expect(files).to.contain('/projects/sandbox/cypress/fixtures/example.json') }) }) + + it('should add @cypress/schematic to the schemaCollections array', async () => { + const tree = await schematicRunner.runSchematicAsync('ng-add', { 'component': true }, appTree).toPromise() + const angularJson = readAngularJson(tree) + const cliOptions = angularJson.cli as JsonObject + + expect(cliOptions).to.eql({ + packageManager: 'yarn', + schematicCollections: ['@cypress/schematic', '@schematics/angular'], + }) + }) + + it('should not overwrite existing schemaCollections array', async () => { + let angularJson = readAngularJson(appTree) + + appTree.overwrite('./angular.json', JSON.stringify({ + ...angularJson, + cli: { + schematicCollections: ['@any/schematic'], + }, + })) + + const tree = await schematicRunner.runSchematicAsync('ng-add', { 'component': true }, appTree).toPromise() + + angularJson = readAngularJson(tree) + const cliOptions = angularJson.cli as JsonObject + + expect(cliOptions).to.eql({ + schematicCollections: ['@cypress/schematic', '@any/schematic', '@schematics/angular'], + }) + }) }) diff --git a/npm/cypress-schematic/src/schematics/ng-add/index.ts b/npm/cypress-schematic/src/schematics/ng-add/index.ts index 8a7953377e99..d3c0dcfd198f 100644 --- a/npm/cypress-schematic/src/schematics/ng-add/index.ts +++ b/npm/cypress-schematic/src/schematics/ng-add/index.ts @@ -45,6 +45,7 @@ export default function (_options: any): Rule { addCtSpecs(_options), addCypressTestScriptsToPackageJson(), modifyAngularJson(_options), + addDefaultSchematic(), ])(tree, _context) } } @@ -306,6 +307,33 @@ function modifyAngularJson (options: any): Rule { } } +function addDefaultSchematic (): Rule { + return (tree: Tree, _: SchematicContext) => { + if (tree.exists('./angular.json')) { + const angularJsonVal = getAngularJsonValue(tree) + const angularSchematic = '@schematics/angular' + const cli = { + ...angularJsonVal.cli, + schematicCollections: ['@cypress/schematic', ...angularJsonVal?.cli?.schematicCollections ?? []], + } + + if (cli.schematicCollections.indexOf('@schematics/angular') === -1) { + cli.schematicCollections.push(angularSchematic) + } + + return tree.overwrite( + './angular.json', + JSON.stringify({ + ...angularJsonVal, + cli, + }, null, 2), + ) + } + + throw new SchematicsException('angular.json not found') + } +} + export const getCypressConfigFile = (angularJsonVal: any, projectName: string) => { const project = angularJsonVal.projects[projectName] const tsConfig = project?.architect?.lint?.options?.tsConfig diff --git a/npm/cypress-schematic/src/schematics/ng-generate/component/index.spec.ts b/npm/cypress-schematic/src/schematics/ng-generate/component/index.spec.ts new file mode 100644 index 000000000000..2c60abc2cf65 --- /dev/null +++ b/npm/cypress-schematic/src/schematics/ng-generate/component/index.spec.ts @@ -0,0 +1,60 @@ +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing' +import { expect } from 'chai' +import { join } from 'path' + +describe('ng-generate @cypress/schematic:component', () => { + const schematicRunner = new SchematicTestRunner( + 'schematics', + join(__dirname, '../../collection.json'), + ) + let appTree: UnitTestTree + + const workspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '12.0.0', + } + + const appOptions: Parameters[2] = { + name: 'sandbox', + inlineTemplate: false, + routing: false, + skipTests: false, + skipPackageJson: false, + } + + beforeEach(async () => { + appTree = await schematicRunner.runExternalSchematicAsync('@schematics/angular', 'workspace', workspaceOptions).toPromise() + appTree = await schematicRunner.runExternalSchematicAsync('@schematics/angular', 'application', appOptions, appTree).toPromise() + }) + + it('should create cypress ct alongside the generated component', async () => { + const tree = await schematicRunner.runSchematicAsync('component', { name: 'foo', project: 'sandbox' }, appTree).toPromise() + + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/foo.component.ts') + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/foo.component.html') + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/foo.component.cy.ts') + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/foo.component.css') + expect(tree.files).not.to.contain('/projects/sandbox/src/app/foo/foo.component.spec.ts') + }) + + it('should not generate component which does exist already', async () => { + let tree = await schematicRunner.runSchematicAsync('component', { name: 'foo', project: 'sandbox' }, appTree).toPromise() + + tree = await schematicRunner.runSchematicAsync('component', { name: 'foo', project: 'sandbox' }, appTree).toPromise() + + expect(tree.files.filter((f) => f === '/projects/sandbox/src/app/foo/foo.component.ts').length).to.eq(1) + expect(tree.files.filter((f) => f === '/projects/sandbox/src/app/foo/foo.component.html').length).to.eq(1) + expect(tree.files.filter((f) => f === '/projects/sandbox/src/app/foo/foo.component.cy.ts').length).to.eq(1) + expect(tree.files.filter((f) => f === '/projects/sandbox/src/app/foo/foo.component.css').length).to.eq(1) + }) + + it('should generate component given a component containing a directory', async () => { + const tree = await schematicRunner.runSchematicAsync('component', { name: 'foo/bar', project: 'sandbox' }, appTree).toPromise() + + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/bar/bar.component.ts') + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/bar/bar.component.html') + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/bar/bar.component.cy.ts') + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/bar/bar.component.css') + }) +}) diff --git a/npm/cypress-schematic/src/schematics/ng-generate/component/index.ts b/npm/cypress-schematic/src/schematics/ng-generate/component/index.ts new file mode 100644 index 000000000000..5f926c54a65e --- /dev/null +++ b/npm/cypress-schematic/src/schematics/ng-generate/component/index.ts @@ -0,0 +1,27 @@ +import { chain, externalSchematic, noop, Rule, SchematicContext, Tree } from '@angular-devkit/schematics' +import cypressTest from '../cypress-test' +import path = require('path'); + +export default function (options: any): Rule { + return (_: Tree, _context: SchematicContext) => { + return chain([ + externalSchematic('@schematics/angular', 'component', { + ...options, + skipTests: true, + }), + (tree: Tree, _context: SchematicContext) => { + const componentName = path.parse(options.name).name + const componentPath = tree.actions.filter((a) => a.path.includes(`${componentName}.component.ts`)) + .map((a) => path.dirname(a.path)) + .at(0) + + return componentPath ? cypressTest({ + ...options, + component: true, + path: componentPath, + name: componentName, + }) : noop() + }, + ]) + } +} diff --git a/npm/cypress-schematic/src/schematics/ng-generate/component/schema.json b/npm/cypress-schematic/src/schematics/ng-generate/component/schema.json new file mode 100644 index 000000000000..01d8f39925cf --- /dev/null +++ b/npm/cypress-schematic/src/schematics/ng-generate/component/schema.json @@ -0,0 +1,152 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "cypress-schematics-generate-component", + "title": "Cypress Wrapper for Angular Component Options Schema", + "type": "object", + "description": "Creates a new, generic component definition in the given or default project.", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "format": "path", + "$default": { + "$source": "workingDirectory" + }, + "description": "The path at which to create the component file, relative to the current workspace. Default is a folder with the same name as the component in the project root.", + "visible": false + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + } + }, + "name": { + "type": "string", + "description": "The name of the component.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use for the component?" + }, + "displayBlock": { + "description": "Specifies if the style will contain `:host { display: block; }`.", + "type": "boolean", + "default": false, + "alias": "b" + }, + "inlineStyle": { + "description": "Include styles inline in the component.ts file. Only CSS styles can be included inline. By default, an external styles file is created and referenced in the component.ts file.", + "type": "boolean", + "default": false, + "alias": "s", + "x-user-analytics": "ep.ng_inline_style" + }, + "inlineTemplate": { + "description": "Include template inline in the component.ts file. By default, an external template file is created and referenced in the component.ts file.", + "type": "boolean", + "default": false, + "alias": "t", + "x-user-analytics": "ep.ng_inline_template" + }, + "standalone": { + "description": "Whether the generated component is standalone.", + "type": "boolean", + "default": false, + "x-user-analytics": "ep.ng_standalone" + }, + "viewEncapsulation": { + "description": "The view encapsulation strategy to use in the new component.", + "enum": [ + "Emulated", + "None", + "ShadowDom" + ], + "type": "string", + "alias": "v" + }, + "changeDetection": { + "description": "The change detection strategy to use in the new component.", + "enum": [ + "Default", + "OnPush" + ], + "type": "string", + "default": "Default", + "alias": "c" + }, + "prefix": { + "type": "string", + "description": "The prefix to apply to the generated component selector.", + "alias": "p", + "oneOf": [ + { + "maxLength": 0 + }, + { + "minLength": 1, + "format": "html-selector" + } + ] + }, + "style": { + "description": "The file extension or preprocessor to use for style files, or 'none' to skip generating the style file.", + "type": "string", + "default": "css", + "enum": [ + "css", + "scss", + "sass", + "less", + "none" + ], + "x-user-analytics": "ep.ng_style" + }, + "type": { + "type": "string", + "description": "Adds a developer-defined type to the filename, in the format \"name.type.ts\".", + "default": "Component" + }, + "skipTests": { + "type": "boolean", + "description": "Do not create \"spec.ts\" test files for the new component.", + "default": false + }, + "flat": { + "type": "boolean", + "description": "Create the new files at the top level of the current project.", + "default": false + }, + "skipImport": { + "type": "boolean", + "description": "Do not import this component into the owning NgModule.", + "default": false + }, + "selector": { + "type": "string", + "format": "html-selector", + "description": "The HTML selector to use for this component." + }, + "skipSelector": { + "type": "boolean", + "default": false, + "description": "Specifies if the component should have a selector or not." + }, + "module": { + "type": "string", + "description": "The declaring NgModule.", + "alias": "m" + }, + "export": { + "type": "boolean", + "default": false, + "description": "The declaring NgModule exports this component." + } + }, + "required": [ + "name", + "project" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index aa6595d04f0a..162eca05ce2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,7 +22,7 @@ "@angular-devkit/core" "14.2.1" rxjs "6.6.7" -"@angular-devkit/core@14.2.1", "@angular-devkit/core@^14.2.1": +"@angular-devkit/core@14.2.1": version "14.2.1" resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-14.2.1.tgz#7ce14efdb5fce687bb4f13bef03d4b67e971b22e" integrity sha512-lW8oNGuJqr4r31FWBjfWQYkSXdiOHBGOThIEtHvUVBKfPF/oVrupLueCUgBPel+NvxENXdo93uPsqHN7bZbmsQ== @@ -33,6 +33,17 @@ rxjs "6.6.7" source-map "0.7.4" +"@angular-devkit/core@14.2.10", "@angular-devkit/core@^14.2.1": + version "14.2.10" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-14.2.10.tgz#9eedb7cf783030252f0c7546ce80fc321633b499" + integrity sha512-K4AO7mROTdbhQ7chtyQd6oPwmuL+BPUh+wn6Aq1qrmYJK4UZYFOPp8fi/Ehs8meCEeywtrssOPfrOE4Gsre9dg== + dependencies: + ajv "8.11.0" + ajv-formats "2.1.1" + jsonc-parser "3.1.0" + rxjs "6.6.7" + source-map "0.7.4" + "@angular-devkit/schematics-cli@^14.2.1": version "14.2.1" resolved "https://registry.yarnpkg.com/@angular-devkit/schematics-cli/-/schematics-cli-14.2.1.tgz#cbea1e4a47880ee4e3eb4816d25a104b4f9fcc10" @@ -45,7 +56,7 @@ symbol-observable "4.0.0" yargs-parser "21.1.1" -"@angular-devkit/schematics@14.2.1", "@angular-devkit/schematics@^14.2.1": +"@angular-devkit/schematics@14.2.1": version "14.2.1" resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-14.2.1.tgz#9d66080e60ab32d1b44c854cabc8f5cbb421d877" integrity sha512-0U18FwDYt4zROBPrvewH6iBTkf2ozVHN4/gxUb9jWrqVw8mPU5AWc/iYxQLHBSinkr2Egjo1H/i9aBqgJSeh3g== @@ -56,6 +67,17 @@ ora "5.4.1" rxjs "6.6.7" +"@angular-devkit/schematics@14.2.10", "@angular-devkit/schematics@^14.2.1": + version "14.2.10" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-14.2.10.tgz#91fcc85199aa7fa9a3a0cb49d8a5266421f9d5e3" + integrity sha512-MMp31KpJTwKHisXOq+6VOXYApq97hZxFaFmZk396X5aIFTCELUwjcezQDk+u2nEs5iK/COUfnN3plGcfJxYhQA== + dependencies: + "@angular-devkit/core" "14.2.10" + jsonc-parser "3.1.0" + magic-string "0.26.2" + ora "5.4.1" + rxjs "6.6.7" + "@angular/cli@^14.2.1": version "14.2.1" resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-14.2.1.tgz#1e4faabf537c2686c34487e3fa3b03affbb31a01" @@ -5578,7 +5600,7 @@ dependencies: any-observable "^0.3.0" -"@schematics/angular@14.2.1", "@schematics/angular@^14.2.1": +"@schematics/angular@14.2.1": version "14.2.1" resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-14.2.1.tgz#00f54cb6b30dc1945b6d344ca225c04dcc12f682" integrity sha512-Dchixep/FMETAMuyFchw9Nryi7tfuZQRumzIOtQpv+KaVtfjvcIlES0KuI0U3Qh7tGIYPBmO3Mkt3oojcl2RBA== @@ -5587,6 +5609,15 @@ "@angular-devkit/schematics" "14.2.1" jsonc-parser "3.1.0" +"@schematics/angular@^14.2.1": + version "14.2.10" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-14.2.10.tgz#676b045ea647e8908ade169ebb688e40b158ca83" + integrity sha512-YFTc/9QJdx422XcApizEcVLKoyknu8b9zHIlAepZCu7WkV8GPT0hvVEHQ7KBWys5aQ7pPZMT0JpZLeAz0F2xYQ== + dependencies: + "@angular-devkit/core" "14.2.10" + "@angular-devkit/schematics" "14.2.10" + jsonc-parser "3.1.0" + "@semantic-release/changelog@5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@semantic-release/changelog/-/changelog-5.0.1.tgz#50a84b63e5d391b7debfe021421589fa2bcdafe4" @@ -25867,9 +25898,9 @@ rxjs@6.6.7, rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.4, rxjs@^6.6.0, rxjs@^6.6.3, rxj tslib "^1.9.0" rxjs@^7.1.0, rxjs@^7.5.5: - version "7.5.6" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc" - integrity sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw== + version "7.5.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" + integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== dependencies: tslib "^2.1.0"