diff --git a/.github/PULL_REQUEST_TEMPATE.md b/.github/PULL_REQUEST_TEMPATE.md index 622e729..bd84c0d 100644 --- a/.github/PULL_REQUEST_TEMPATE.md +++ b/.github/PULL_REQUEST_TEMPATE.md @@ -1,14 +1,19 @@ ## Description + ### What's included? + + - One - Two - Three #### Test Steps + + - [ ] `npm ci` - [ ] In `proxy.conf.js` file, change serverUrl to `https://appcenter.ux.ac.uda.io` - [ ] `npm run serve` diff --git a/package.json b/package.json index 772b257..0eb4f3c 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..854d187 --- /dev/null +++ b/scripts/test-schematics.sh @@ -0,0 +1,24 @@ +# 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 +cd testxyz +ng add @angular/material +ng add @covalent/core + +# 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-lock.json b/src/lib/package-lock.json new file mode 100644 index 0000000..93dcfca --- /dev/null +++ b/src/lib/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "@td-vantage/ui-platform", + "version": "0.0.0-PLATFORM", + "lockfileVersion": 1 +} 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..272383b --- /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"] + } + } +} 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..a33c54c --- /dev/null +++ b/src/lib/schematics/ng-add/files/proxy.conf.js @@ -0,0 +1,27 @@ +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..26cce39 --- /dev/null +++ b/src/lib/schematics/ng-add/files/src/app/app.routes.ts @@ -0,0 +1,16 @@ +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..297b3b0 --- /dev/null +++ b/src/lib/schematics/ng-add/index.ts @@ -0,0 +1,122 @@ + +import { + Rule, + chain, + Tree, + mergeWith, + url, + apply, + branchAndMerge, + template, + UpdateRecorder, + FileEntry, + SchematicContext, +} 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, getProjectStyleFile } from '@angular/cdk/schematics/utils'; + +import { SourceFile } from 'typescript'; +import { Change } from '@schematics/angular/utility/change'; + +export function addDependenciesAndFiles(options: ISchema): Rule { + const addVantagePacakgeRule: Rule = (host: Tree) => { + addPackageToPackageJson(host, '@td-vantage/ui-platform', `${uiPlatformVersion}`); + }; + + const ruleSet: Rule[] = [addVantagePacakgeRule]; + + if (options.ssoServerURL && options.ssoServerURL.trim().length) { + // enable SSO + ruleSet.push(mergeFiles(options)); + ruleSet.push(addSSOImports); + ruleSet.push(updateStyles); + } + return chain(ruleSet); +} + +function mergeFiles(options: ISchema): Rule { + const templateSource: any = apply(url('./files'), [ + template({ + ...strings, + ...options, + }), + ]); + return branchAndMerge(mergeWith(templateSource)); +} + +function updateStyles(): Rule { + return (host: Tree, context: SchematicContext) => { + const workspace: experimental.workspace.WorkspaceSchema = getWorkspace(host); + const project: experimental.workspace.WorkspaceProject = getProjectFromWorkspace(workspace); + const styleFilePath: string = getProjectStyleFile(project); + const file: Buffer = host.read(styleFilePath); + const themeFile: FileEntry = host.get('theme.scss'); + const fileContent: string = file.toString(); + const content: string = themeFile && themeFile.content.toString(); + + if (content) { + host.overwrite(styleFilePath, fileContent + '\n' + content); + host.delete('theme.scss'); + } + + return host; + }; +} + +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..2b5a1a0 --- /dev/null +++ b/src/lib/schematics/ng-add/schema.ts @@ -0,0 +1,4 @@ +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..41441a0 --- /dev/null +++ b/src/lib/schematics/tsconfig.json @@ -0,0 +1,23 @@ +{ + "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/**/*"] +} diff --git a/src/lib/schematics/tsconfig.spec.json b/src/lib/schematics/tsconfig.spec.json new file mode 100644 index 0000000..9775e29 --- /dev/null +++ b/src/lib/schematics/tsconfig.spec.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "./", + "lib": ["es6", "dom"], + "module": "commonjs", + "types": ["jasmine", "hammerjs", "node"] + }, + "include": ["**/*", "**/*.spec.ts"], + "exclude": ["*/files/**/*", "*/files/**/**/*"] +} 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);