diff --git a/package.json b/package.json index 57701f2..569a966 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "stylelint": "./node_modules/.bin/stylelint 'src/**/*.scss' '!**/assets/**' --config stylelint.config.js --syntax scss", "webdriver-update": "bash ./node_modules/.bin/webdriver-manager update", "test": "ng test ui-platform --code-coverage --source-map=false --watch=false", + "test:schematics": "tsc -p src/lib/schematics/tsconfig.spec.json && jasmine src/lib/schematics/**/*.spec.js", "coveralls": "cat ./coverage/lcov.info | node ./node_modules/coveralls/bin/coveralls.js", "build:lib": "bash scripts/build-release && gulp version-placeholder", "release:start": "bash scripts/start-release", diff --git a/scripts/test-schematics.sh b/scripts/test-schematics.sh new file mode 100755 index 0000000..ca09fdf --- /dev/null +++ b/scripts/test-schematics.sh @@ -0,0 +1,21 @@ +# Run test and linter +npm run test:schematics +npm run tslint + +# Link project +cd ../src/lib +npm link + +# Create Angular base project +cd /tmp +rm -rf testxyz +ng new testxyz + +# Run covalent schematics +cd testxyz +npm link @td-vantage/ui-platform +ng g @td-vantage/ui-platform:ng-add + +# Check generated files +git status +npm i diff --git a/src/lib/package.json b/src/lib/package.json index c337996..fdcc21b 100644 --- a/src/lib/package.json +++ b/src/lib/package.json @@ -10,6 +10,7 @@ }, "license": "MIT", "author": "Teradata UI Team", + "schematics": "./schematics/collection.json", "peerDependencies": { "@angular/common": "^0.0.0-NG", "@angular/core": "^0.0.0-NG", diff --git a/src/lib/schematics/.gitignore b/src/lib/schematics/.gitignore new file mode 100644 index 0000000..57d1127 --- /dev/null +++ b/src/lib/schematics/.gitignore @@ -0,0 +1,4 @@ +# Outputs +**/*.js +**/*.js.map +**/*.d.ts \ No newline at end of file diff --git a/src/lib/schematics/collection.json b/src/lib/schematics/collection.json new file mode 100644 index 0000000..5ee1182 --- /dev/null +++ b/src/lib/schematics/collection.json @@ -0,0 +1,11 @@ +{ + "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json", + "schematics": { + "ng-add": { + "description": "Adds vantage ui platform to the application without affecting any templates", + "factory": "./ng-add/index#addDependenciesAndFiles", + "schema": "./ng-add/schema.json", + "aliases": ["vantage-shell", "install"] + } + } + } \ No newline at end of file diff --git a/src/lib/schematics/ng-add/files/proxy.conf.js b/src/lib/schematics/ng-add/files/proxy.conf.js new file mode 100644 index 0000000..382338f --- /dev/null +++ b/src/lib/schematics/ng-add/files/proxy.conf.js @@ -0,0 +1,26 @@ +const vantageLoginProxyConfig = require('./src/lib/auth/config/vantageLoginProxyConfig'); + +/* * * * * * * * * * * */ +/* Edit these variables to point to your */ +/* Vantage and local development environments */ +/* * * * * * * * * * * */ + +const serverUrl = 'https://vantage.url.io'; // REPLACE WITH VANTAGE BASE URL +const localUrl = "localhost:4200"; +const localProto = "http"; // http or https + +/* * * * * * * * * * * */ +/* This section contains the routes proxied through */ +/* your local development environment and the Vantage deployment */ +/* * * * * * * * * * * */ + +const PROXY_CONFIG = { + ...vantageLoginProxyConfig({ serverUrl, localUrl, localProto }), + '/api': { + target: serverUrl, + secure: false, + changeOrigin: true, + }, +}; + +module.exports = PROXY_CONFIG; diff --git a/src/lib/schematics/ng-add/files/src/app/app.routes.ts b/src/lib/schematics/ng-add/files/src/app/app.routes.ts new file mode 100644 index 0000000..e077bec --- /dev/null +++ b/src/lib/schematics/ng-add/files/src/app/app.routes.ts @@ -0,0 +1,18 @@ +import { Routes, RouterModule } from '@angular/router'; + +import { VantageAuthenticationGuard } from '@td-vantage/ui-platform/auth'; + +const routes: Routes = [ + { + path: '', + canActivate: [VantageAuthenticationGuard], + children: [], + }, + { path: '**', redirectTo: '/' }, +]; + +export const appRoutingProviders: any[] = [ + VantageAuthenticationGuard, +]; + +export const appRoutes: any = RouterModule.forRoot(routes); diff --git a/src/lib/schematics/ng-add/index.spec.ts b/src/lib/schematics/ng-add/index.spec.ts new file mode 100644 index 0000000..d04136a --- /dev/null +++ b/src/lib/schematics/ng-add/index.spec.ts @@ -0,0 +1,84 @@ +import { getFileContent } from '@schematics/angular/utility/test'; +import { Tree } from '@angular-devkit/schematics'; +import { uiPlatformVersion } from '../version-names'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { Schema as WorkspaceOptions } from '@schematics/angular/workspace/schema'; +import { Schema as ApplicationOptions } from '@schematics/angular/application/schema'; + +const collectionPath: string = require.resolve('../collection.json'); + +describe('ng-add schematic', () => { + const testRunner: SchematicTestRunner = new SchematicTestRunner('rocket', collectionPath); + + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '1.0.0', + }; + + const appOptions: ApplicationOptions = { + name: 'ui-platform-workspace', + }; + + let appTree: UnitTestTree; + + beforeEach(async () => { + const workspaceTree: UnitTestTree = await testRunner + .runExternalSchematicAsync('@schematics/angular', 'workspace', workspaceOptions) + .toPromise(); + appTree = await testRunner + .runExternalSchematicAsync('@schematics/angular', 'application', appOptions, workspaceTree) + .toPromise(); + }); + + it('should update package.json', async () => { + const tree: Tree = await testRunner.runSchematicAsync('ng-add', undefined, appTree).toPromise(); + const packageJson: any = JSON.parse(getFileContent(tree, '/package.json')); + const dependencies: any = packageJson.dependencies; + + const expectedUIPlatformVersion: string = `${uiPlatformVersion}`; + + expectVersionToBe(dependencies, '@td-vantage/ui-platform', expectedUIPlatformVersion); + }); + + it('should create proxy.conf.js when sso option is selected by user', async () => { + const dependencyOptions: any = { ssoServerURL: 'https://vantage.url.io' }; + const tree: Tree = await testRunner.runSchematicAsync('ng-add', dependencyOptions, appTree).toPromise(); + expect(tree.exists('proxy.conf.js')).toBe(true); + const fileContent: string = getFileContent(tree, 'proxy.conf.js'); + expect(fileContent).toContain('https://vantage.url.io'); + }); + + it('should import Vantage Auth modules to app.module.ts when sso option is selected by user', async () => { + const dependencyOptions: any = { ssoServerURL: 'https://vantage.url.io' }; + const tree: Tree = await testRunner.runSchematicAsync('ng-add', dependencyOptions, appTree).toPromise(); + const fileContent: string = getFileContent(tree, 'projects/ui-platform-workspace/src/app/app.module.ts'); + expect(fileContent).toContain('VantageAuthenticationModule'); + expect(fileContent).toContain('VantageAuthenticationInterceptor'); + expect(fileContent).toContain('VantageUserModule'); + expect(fileContent).toContain('CovalentHttpModule'); + }); + + it('should create app.routes.ts when sso option is selected by user', async () => { + const dependencyOptions: any = { ssoServerURL: 'https://vantage.url.io' }; + const tree: Tree = await testRunner.runSchematicAsync('ng-add', dependencyOptions, appTree).toPromise(); + expect(tree.exists('src/app/app.routes.ts')).toBe(true); + const fileContent: string = getFileContent(tree, 'src/app/app.routes.ts'); + expect(fileContent).toContain('VantageAuthenticationGuard'); + }); + + it('should import route provider to app.module.ts when sso option is selected by user', async () => { + const dependencyOptions: any = { ssoServerURL: 'https://vantage.url.io' }; + const tree: Tree = await testRunner.runSchematicAsync('ng-add', dependencyOptions, appTree).toPromise(); + const fileContent: string = getFileContent(tree, 'projects/ui-platform-workspace/src/app/app.module.ts'); + expect(fileContent).toContain('appRoutes'); + expect(fileContent).toContain('appRoutingProviders'); + }); + + function expectVersionToBe(dependencies: any, name: string, expectedVersion: string): void { + expect(dependencies[name]).toBe( + expectedVersion, + 'Expected ' + name + ' package to have ' + `${expectedVersion}` + ' version.', + ); + } +}); diff --git a/src/lib/schematics/ng-add/index.ts b/src/lib/schematics/ng-add/index.ts new file mode 100644 index 0000000..ef6e491 --- /dev/null +++ b/src/lib/schematics/ng-add/index.ts @@ -0,0 +1,87 @@ +import { Rule, chain, Tree, mergeWith, url, apply, branchAndMerge, SchematicsException, template, UpdateRecorder } from '@angular-devkit/schematics'; +import { addPackageToPackageJson } from '@angular/material/schematics/ng-add/package-config'; +import { uiPlatformVersion } from '../version-names'; +import { ISchema } from './schema'; +import { strings } from '@angular-devkit/core'; + +import { getProjectFromWorkspace, addModuleImportToRootModule } from '@angular/cdk/schematics'; +import { addProviderToModule } from '@schematics/angular/utility/ast-utils'; +import { InsertChange } from '@schematics/angular/utility/change'; +import { getAppModulePath } from '@schematics/angular/utility/ng-ast-utils'; +import { getWorkspace } from '@schematics/angular/utility/config'; +import { experimental } from '@angular-devkit/core'; +import { getSourceFile, getProjectMainFile } from '@angular/cdk/schematics/utils'; +import { SourceFile } from 'typescript'; +import { Change } from '@schematics/angular/utility/change'; + +export function addDependenciesAndFiles(options: ISchema): Rule { + let addVantagePacakgeRule: Rule = (host: Tree) => { + addPackageToPackageJson(host, '@td-vantage/ui-platform', `${uiPlatformVersion}`); + }; + + let ruleSet: Rule[] = [addVantagePacakgeRule]; + + if (options.ssoServerURL && options.ssoServerURL.trim().length) { // enable SSO + ruleSet.push(mergeFiles(options)); + ruleSet.push(addSSOImports); + } + return chain(ruleSet); +} + +function mergeFiles(options: ISchema): Rule { + const templateSource: any = apply(url('./files'), [ + template({ + ...strings, + ...options, + }), + ]); + return branchAndMerge(mergeWith(templateSource)); +} + +function addSSOImports(): Rule { + return (host: Tree) => { + const workspace: experimental.workspace.WorkspaceSchema = getWorkspace(host); + const project: experimental.workspace.WorkspaceProject = getProjectFromWorkspace(workspace); + const replacementString: string = `CovalentHttpModule.forRoot({ + interceptors: [{ + interceptor: VantageAuthenticationInterceptor, paths: ['**'], + }], + })`; + + addModuleImportToRootModule(host, 'VantageAuthenticationModule', '@td-vantage/ui-platform/auth', project); + addModuleImportToRootModule(host, 'VantageUserModule', '@td-vantage/ui-platform/user', project); + addModuleImportToRootModule(host, `CovalentHttpModule.forRoot()`, '@covalent/http', project); + replaceContentInAppModule(host, `CovalentHttpModule.forRoot()`, replacementString); + addModuleImportToRootModule(host, 'appRoutes', './app.routes', project); + + addProvider(host, `VantageAuthenticationInterceptor`, '@td-vantage/ui-platform/auth'); + addProvider(host, `appRoutingProviders`, './app.routes'); + }; +} + +function addProvider(host: Tree, classifiedName: string, importPath: string): void { + const workspace: experimental.workspace.WorkspaceSchema = getWorkspace(host); + const project: experimental.workspace.WorkspaceProject = getProjectFromWorkspace(workspace); + const modulePath: string = getAppModulePath(host, getProjectMainFile(project)); + const moduleSource: SourceFile = getSourceFile(host, modulePath); + const changes: Change[] = addProviderToModule(moduleSource, modulePath, classifiedName, importPath); + applyChanges(host, modulePath, changes); +} + +function applyChanges(tree: Tree, path: string, changes: Change[]): void { + const recorder: UpdateRecorder = tree.beginUpdate(path); + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + tree.commitUpdate(recorder); +} + +function replaceContentInAppModule(host: Tree, match: string, replacement: string): void { + const workspace: experimental.workspace.WorkspaceSchema = getWorkspace(host); + const project: experimental.workspace.WorkspaceProject = getProjectFromWorkspace(workspace); + const modulePath: string = getAppModulePath(host, getProjectMainFile(project)); + const content: string = host.get(modulePath).content.toString(); + host.overwrite(modulePath, content.replace(match, replacement)); +} diff --git a/src/lib/schematics/ng-add/schema.json b/src/lib/schematics/ng-add/schema.json new file mode 100644 index 0000000..9460edc --- /dev/null +++ b/src/lib/schematics/ng-add/schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "vantage-ui-platform-ng-add", + "title": "Vantage UI Platform ng-add schematic", + "type": "object", + "properties": { + "ssoServerURL": { + "type": "string", + "description": "Add SSO if user provides SSO server URL", + "x-prompt": "Add SSO server url to setup SSO or press enter to skip" + } + }, + "required": [] +} diff --git a/src/lib/schematics/ng-add/schema.ts b/src/lib/schematics/ng-add/schema.ts new file mode 100644 index 0000000..e0252a9 --- /dev/null +++ b/src/lib/schematics/ng-add/schema.ts @@ -0,0 +1,5 @@ +export interface ISchema { + /** Whether SSO should be set up. */ + ssoServerURL: string; + +} diff --git a/src/lib/schematics/tsconfig.json b/src/lib/schematics/tsconfig.json new file mode 100644 index 0000000..972d367 --- /dev/null +++ b/src/lib/schematics/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "rootDir": "./", + "baseUrl": "./", + "declaration": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "outDir": "../../../../deploy/ui-platform/schematics", + "lib": ["es2017", "dom"], + "moduleResolution": "node", + "sourceMap": true, + "target": "es5", + "typeRoots": ["./../../../../node_modules/@types"], + "noUnusedParameters": false, + "noUnusedLocals": false, + "allowUnreachableCode": false, + "pretty": true, + "importHelpers": true + }, + "include": ["**/*"], + "exclude": ["**/*.spec.ts", "**/files/**/*"] + } + \ No newline at end of file diff --git a/src/lib/schematics/tsconfig.spec.json b/src/lib/schematics/tsconfig.spec.json new file mode 100644 index 0000000..b90b028 --- /dev/null +++ b/src/lib/schematics/tsconfig.spec.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "./", + "lib": ["es6", "dom"], + "module": "commonjs", + "types": ["jasmine", "hammerjs", "node"] + }, + "include": ["**/*", "**/*.spec.ts"], + "exclude": ["*/files/**/*", "*/files/**/**/*"] + } + \ No newline at end of file diff --git a/src/lib/schematics/version-names.ts b/src/lib/schematics/version-names.ts new file mode 100644 index 0000000..50316eb --- /dev/null +++ b/src/lib/schematics/version-names.ts @@ -0,0 +1 @@ +export const uiPlatformVersion: string = '1.0.0-beta.0'; diff --git a/src/lib/test.ts b/src/lib/test.ts index e51f448..71011bf 100644 --- a/src/lib/test.ts +++ b/src/lib/test.ts @@ -9,6 +9,6 @@ declare const require: any; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); // Then we find all the tests. -const context: any = require.context('./', true, /\.spec\.ts$/); +const context: any = require.context('./', true, /^(?!.*(\/schematics\/)).*\.spec\.ts$/); // And load the modules. context.keys().map(context);