From fda89bf6228a815465dd5adb6ef27fc23e5e2169 Mon Sep 17 00:00:00 2001 From: Yohan Lasorsa Date: Thu, 8 Aug 2019 10:57:12 +0200 Subject: [PATCH 01/10] doc: fix local development instructions --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87d4302..5f5c006 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,10 +58,10 @@ Once you have completed the previous steps to npm link the local copy of ng-depl 1. You may be prompted you to sign in to Azure, providing a link to open in your browser and a code to paste in the login page. -1. Then, instead of running `ng add @azure/ng-deploy`, add the local version. +1. Then, running `ng add @azure/ng-deploy` will use the locally linked version. ```sh - ng add ng-deploy-azure + ng add @azure/ng-deploy ``` 1. Now you can deploy your angular app to azure. From 6131357739ab212157f0b7db007bb03bd0e7d7e7 Mon Sep 17 00:00:00 2001 From: Yohan Lasorsa Date: Thu, 8 Aug 2019 11:05:24 +0200 Subject: [PATCH 02/10] chore: remove unused packages and files --- package.json | 11 --------- tslint.json | 69 ---------------------------------------------------- 2 files changed, 80 deletions(-) delete mode 100644 tslint.json diff --git a/package.json b/package.json index 7f31aa5..8c8b832 100644 --- a/package.json +++ b/package.json @@ -59,11 +59,8 @@ "@azure/storage-blob": "^10.3.0", "adal-node": "^0.1.28", "chalk": "^2.4.2", - "cli-ux": "^5.2.1", "conf": "^3.0.0", - "configstore": "^4.0.0", "fuzzy": "^0.1.3", - "gfycat-style-urls": "^1.0.3", "glob": "^7.1.3", "inquirer": "^6.2.2", "inquirer-autocomplete-prompt": "^1.0.1", @@ -71,27 +68,19 @@ "ora": "^3.4.0", "progress": "^2.0.3", "promise-limit": "^2.7.0", - "tslib": "^1.9.3", "typescript": "~3.4.0" }, "devDependencies": { "@schematics/angular": "^8.2.0", - "@types/chai": "^4.0.4", "@types/conf": "^2.1.0", - "@types/configstore": "^4.0.0", "@types/glob": "^7.1.1", "@types/inquirer": "0.0.44", - "@types/jasmine": "^3.3.12", "@types/jest": "^24.0.13", "@types/mime-types": "^2.1.0", - "@types/mocha": "^5.2.5", "@types/node": "^10.12.18", - "jasmine": "^3.0.0", "jest": "^24.8.0", - "prettier": "^1.16.4", "schematics-utilities": "^1.1.2", "ts-jest": "^24.0.2", - "tslint-angular": "^1.1.2", "typescript": "~3.4.0" } } diff --git a/tslint.json b/tslint.json deleted file mode 100644 index bac6200..0000000 --- a/tslint.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "rulesDirectory": ["codelyzer"], - "rules": { - "arrow-return-shorthand": true, - "callable-types": true, - "class-name": true, - "comment-format": [true, "check-space"], - "curly": true, - "deprecation": { - "severity": "warn" - }, - "eofline": true, - "forin": true, - "import-blacklist": [true, "rxjs/Rx"], - "import-spacing": true, - "indent": [true, "spaces"], - "interface-over-type-literal": true, - "label-position": true, - "max-classes-per-file": [true, 1], - "max-line-length": [true, 140], - "member-access": false, - "member-ordering": [ - true, - { - "order": ["static-field", "instance-field", "static-method", "instance-method"] - } - ], - "no-arg": true, - "no-bitwise": true, - "no-construct": true, - "no-debugger": true, - "no-duplicate-super": true, - "no-empty": false, - "no-empty-interface": true, - "no-eval": true, - "no-inferrable-types": [true, "ignore-params"], - "no-misused-new": true, - "no-non-null-assertion": true, - "no-shadowed-variable": true, - "no-string-literal": false, - "no-string-throw": true, - "no-switch-case-fall-through": true, - "no-trailing-whitespace": true, - "no-unnecessary-initializer": true, - "no-unused-expression": true, - "no-use-before-declare": true, - "no-var-keyword": true, - "object-literal-sort-keys": false, - "one-line": [true, "check-open-brace", "check-catch", "check-else", "check-whitespace"], - "prefer-const": true, - "quotemark": [true, "single"], - "radix": true, - "semicolon": [true, "always"], - "triple-equals": [true, "allow-null-check"], - "typedef-whitespace": [ - true, - { - "call-signature": "nospace", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace" - } - ], - "unified-signatures": true, - "variable-name": false, - "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type"] - } -} From 6a730cc9678a32b63cc40b1859c8dd3c9d82d8ea Mon Sep 17 00:00:00 2001 From: Yohan Lasorsa Date: Thu, 8 Aug 2019 11:07:47 +0200 Subject: [PATCH 03/10] chore: add keywords & contributor --- package.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8c8b832..1750bff 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,10 @@ "copy:ngadd:json": "cp ./src/ng-add/*.json ./out/ng-add" }, "keywords": [ - "schematics" + "schematics", + "angular", + "azure", + "deploy" ], "author": { "name": "Shmuela Jacobs", @@ -32,6 +35,10 @@ { "name": "Chris Noring", "url": "https://twitter.com/chris_noring" + }, + { + "name": "Yohan Lasorsa", + "url": "https://twitter.com/sinedied" } ], "homepage": "https://github.com/Azure/ng-deploy-azure/", From af7cda391a1eca30f788485a742365479264f71d Mon Sep 17 00:00:00 2001 From: Yohan Lasorsa Date: Thu, 8 Aug 2019 11:39:25 +0200 Subject: [PATCH 04/10] test: move jest config to package.json --- jest.config.js | 12 ------------ package.json | 6 ++++++ 2 files changed, 6 insertions(+), 12 deletions(-) delete mode 100644 jest.config.js diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index d7ea70f..0000000 --- a/jest.config.js +++ /dev/null @@ -1,12 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -module.exports = { - "roots": [ - "/src" - ], - "transform": { - "^.+\\.tsx?$": "ts-jest" - }, -}; diff --git a/package.json b/package.json index 1750bff..1d9e153 100644 --- a/package.json +++ b/package.json @@ -89,5 +89,11 @@ "schematics-utilities": "^1.1.2", "ts-jest": "^24.0.2", "typescript": "~3.4.0" + }, + "jest": { + "roots": ["/src"], + "transform": { + "^.+\\.tsx?$": "ts-jest" + } } } From a79b09d93dd059550fd49004f59690e2ff2e9e1f Mon Sep 17 00:00:00 2001 From: Yohan Lasorsa Date: Thu, 8 Aug 2019 11:40:37 +0200 Subject: [PATCH 05/10] refactor: use @types/progress --- package.json | 5 ++++- src/@types/progress.d.ts | 1 - 2 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 src/@types/progress.d.ts diff --git a/package.json b/package.json index 1d9e153..50f9141 100644 --- a/package.json +++ b/package.json @@ -85,13 +85,16 @@ "@types/jest": "^24.0.13", "@types/mime-types": "^2.1.0", "@types/node": "^10.12.18", + "@types/progress": "^2.0.3", "jest": "^24.8.0", "schematics-utilities": "^1.1.2", "ts-jest": "^24.0.2", "typescript": "~3.4.0" }, "jest": { - "roots": ["/src"], + "roots": [ + "/src" + ], "transform": { "^.+\\.tsx?$": "ts-jest" } diff --git a/src/@types/progress.d.ts b/src/@types/progress.d.ts deleted file mode 100644 index f860166..0000000 --- a/src/@types/progress.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'progress'; From 29070e385c399d5ec05ce53f06598b7e21953bfe Mon Sep 17 00:00:00 2001 From: Yohan Lasorsa Date: Thu, 8 Aug 2019 12:04:57 +0200 Subject: [PATCH 06/10] style: add prettier formatting --- package.json | 16 +- src/builders/actions/deploy.ts | 225 +++++++------- src/builders/deploy.builder.ts | 54 ++-- src/builders/logout.builder.ts | 10 +- src/ng-add/index.spec.ts | 60 ++-- src/ng-add/index.ts | 72 +++-- src/util/azure/__mocks__/auth.ts | 9 +- .../azure/__mocks__/resource-group-helper.ts | 36 +-- src/util/azure/__mocks__/resource-group.ts | 11 +- src/util/azure/__mocks__/subscription.ts | 3 +- src/util/azure/__tests__/locations.ts | 2 +- src/util/azure/__tests__/resource-group.ts | 13 +- src/util/azure/__tests__/subscription.ts | 49 ++-- src/util/azure/account.ts | 274 ++++++++---------- src/util/azure/auth.ts | 67 ++--- src/util/azure/locations.ts | 246 ++++++++-------- src/util/azure/resource-group-helper.ts | 15 +- src/util/azure/resource-group.ts | 118 ++++---- src/util/azure/subscription.ts | 95 +++--- src/util/prompt/__mocks__/spinner.ts | 3 +- src/util/prompt/list.ts | 122 ++++---- src/util/prompt/name-generator.ts | 16 +- src/util/prompt/spinner.ts | 48 ++- src/util/shared/types.ts | 34 +-- src/util/workspace/angular-json.ts | 200 ++++++------- src/util/workspace/azure-json.ts | 84 +++--- 26 files changed, 936 insertions(+), 946 deletions(-) diff --git a/package.json b/package.json index 50f9141..a7d847c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "scripts": { "build": "tsc -p tsconfig.json && npm run copy:builders:json && npm run copy:ngadd:json && tsc -p tsconfig.json", "start": "npm run build:watch", - "build:watch": "npm run build -- -w", + "build:watch": "npm run build -s -- -w", + "format": "npm run format:check -s -- --write", + "format:check": "prettier -l \"./src/**/*.{json,ts}\"", "test:jest": "jest", "test:jest:watch": "jest --watch", "copy:builders:json": "cp ./src/builders/*.json ./out/builders", @@ -86,7 +88,10 @@ "@types/mime-types": "^2.1.0", "@types/node": "^10.12.18", "@types/progress": "^2.0.3", + "husky": "^3.0.2", "jest": "^24.8.0", + "prettier": "^1.18.2", + "pretty-quick": "^1.11.1", "schematics-utilities": "^1.1.2", "ts-jest": "^24.0.2", "typescript": "~3.4.0" @@ -98,5 +103,14 @@ "transform": { "^.+\\.tsx?$": "ts-jest" } + }, + "prettier": { + "singleQuote": true, + "printWidth": 120 + }, + "husky": { + "hooks": { + "pre-commit": "pretty-quick --staged" + } } } diff --git a/src/builders/actions/deploy.ts b/src/builders/actions/deploy.ts index c08c4bb..502f933 100644 --- a/src/builders/actions/deploy.ts +++ b/src/builders/actions/deploy.ts @@ -7,13 +7,13 @@ import * as path from 'path'; import * as glob from 'glob'; import { lookup, charset } from 'mime-types'; import { - uploadStreamToBlockBlob, - Aborter, - BlobURL, - BlockBlobURL, - ContainerURL, - ServiceURL, - SharedKeyCredential + uploadStreamToBlockBlob, + Aborter, + BlobURL, + BlockBlobURL, + ContainerURL, + ServiceURL, + SharedKeyCredential } from '@azure/storage-blob'; import * as promiseLimit from 'promise-limit'; import * as ProgressBar from 'progress'; @@ -24,131 +24,136 @@ import { getAccountKey } from '../../util/azure/account'; import chalk from 'chalk'; import { loginToAzure } from '../../util/azure/auth'; -export default async function deploy(context: BuilderContext, projectRoot: string, azureHostingConfig?: AzureHostingConfig) { - if (!azureHostingConfig) { - throw new Error('Cannot find Azure hosting config for your app in azure.json'); +export default async function deploy( + context: BuilderContext, + projectRoot: string, + azureHostingConfig?: AzureHostingConfig +) { + if (!azureHostingConfig) { + throw new Error('Cannot find Azure hosting config for your app in azure.json'); + } + + if ( + !azureHostingConfig.app || + !azureHostingConfig.azureHosting || + !azureHostingConfig.azureHosting.subscription || + !azureHostingConfig.azureHosting.resourceGroupName || + !azureHostingConfig.azureHosting.account || + !azureHostingConfig.app.project || + !azureHostingConfig.app.target + ) { + throw new Error( + 'Azure hosting config is missing some details. Please run "ng add ng-deploy-azure" and select a storage account.' + ); + } + + const auth = await loginToAzure(context.logger); + const credentials = await auth.credentials; + + context.logger.info('Preparing for deployment'); + + const filesPath = path.join(projectRoot, azureHostingConfig.app.path); + let files = await getFiles(context, filesPath, projectRoot); + + if (files.length === 0) { + // build the project + + context.logger.info(`The folder ${azureHostingConfig.app.path} is empty.`); + if (!context.target) { + throw new Error('Cannot execute the target'); } - if ( - !azureHostingConfig.app || - !azureHostingConfig.azureHosting || - !azureHostingConfig.azureHosting.subscription || - !azureHostingConfig.azureHosting.resourceGroupName || - !azureHostingConfig.azureHosting.account || - !azureHostingConfig.app.project || - !azureHostingConfig.app.target - ) { - throw new Error('Azure hosting config is missing some details. Please run "ng add ng-deploy-azure" and select a storage account.'); + const target: Target = { + target: azureHostingConfig.app.target, + project: context.target.project + }; + if (azureHostingConfig.app.configuration) { + target.configuration = azureHostingConfig.app.configuration; } + context.logger.info(`📦 Running "${azureHostingConfig.app.target}" on "${context.target.project}"`); - const auth = await loginToAzure(context.logger); - const credentials = await auth.credentials; - - context.logger.info('Preparing for deployment'); - - const filesPath = path.join(projectRoot, azureHostingConfig.app.path); - let files = await getFiles(context, filesPath, projectRoot); + const run = await context.scheduleTarget(target); + await run.result; + files = await getFiles(context, filesPath, projectRoot); if (files.length === 0) { - // build the project - - context.logger.info(`The folder ${ azureHostingConfig.app.path } is empty.`); - if (!context.target) { - throw new Error('Cannot execute the target'); - } - - const target: Target = { - target: azureHostingConfig.app.target, - project: context.target.project - }; - if (azureHostingConfig.app.configuration) { - target.configuration = azureHostingConfig.app.configuration; - } - context.logger.info(`📦 Running "${ azureHostingConfig.app.target }" on "${ context.target.project }"`); - - const run = await context.scheduleTarget(target); - await run.result; - - files = await getFiles(context, filesPath, projectRoot); - if (files.length === 0) { - throw new Error('Target did not produce any files, or the path is incorrect.'); - } + throw new Error('Target did not produce any files, or the path is incorrect.'); } + } - const client = new StorageManagementClient(credentials, azureHostingConfig.azureHosting.subscription); - const accountKey = await getAccountKey( - azureHostingConfig.azureHosting.account, client, azureHostingConfig.azureHosting.resourceGroupName); + const client = new StorageManagementClient(credentials, azureHostingConfig.azureHosting.subscription); + const accountKey = await getAccountKey( + azureHostingConfig.azureHosting.account, + client, + azureHostingConfig.azureHosting.resourceGroupName + ); - const pipeline = ServiceURL.newPipeline( - new SharedKeyCredential(azureHostingConfig.azureHosting.account, accountKey) - ); + const pipeline = ServiceURL.newPipeline(new SharedKeyCredential(azureHostingConfig.azureHosting.account, accountKey)); - const serviceURL = new ServiceURL( - `https://${ azureHostingConfig.azureHosting.account }.blob.core.windows.net`, - pipeline - ); + const serviceURL = new ServiceURL( + `https://${azureHostingConfig.azureHosting.account}.blob.core.windows.net`, + pipeline + ); - await uploadFilesToAzure(serviceURL, context, filesPath, files); + await uploadFilesToAzure(serviceURL, context, filesPath, files); - const accountProps = await client.storageAccounts.getProperties( - azureHostingConfig.azureHosting.resourceGroupName, azureHostingConfig.azureHosting.account); - const endpoint = accountProps.primaryEndpoints && accountProps.primaryEndpoints.web; + const accountProps = await client.storageAccounts.getProperties( + azureHostingConfig.azureHosting.resourceGroupName, + azureHostingConfig.azureHosting.account + ); + const endpoint = accountProps.primaryEndpoints && accountProps.primaryEndpoints.web; - context.logger.info( - chalk.green(`see your deployed site at ${ endpoint }`) - ); - // TODO: log url for account at Azure portal + context.logger.info(chalk.green(`see your deployed site at ${endpoint}`)); + // TODO: log url for account at Azure portal } function getFiles(context: BuilderContext, filesPath: string, projectRoot: string) { - - return glob.sync(`**`, { - ignore: ['.git', '.azez.json'], - cwd: filesPath, - nodir: true, - }); + return glob.sync(`**`, { + ignore: ['.git', '.azez.json'], + cwd: filesPath, + nodir: true + }); } export async function uploadFilesToAzure( - serviceURL: ServiceURL, - context: BuilderContext, - filesPath: string, - files: string[] + serviceURL: ServiceURL, + context: BuilderContext, + filesPath: string, + files: string[] ): Promise { - context.logger.info('preparing static deploy'); - const containerURL = ContainerURL.fromServiceURL(serviceURL, '$web'); + context.logger.info('preparing static deploy'); + const containerURL = ContainerURL.fromServiceURL(serviceURL, '$web'); - const bar = new ProgressBar( - '[:bar] :current/:total files uploaded | :percent done | :elapseds | eta: :etas', - { total: files.length } + const bar = new ProgressBar('[:bar] :current/:total files uploaded | :percent done | :elapseds | eta: :etas', { + total: files.length + }); + + bar.tick(0); + + await promiseLimit(5).map(files, async function(file: string) { + const blobURL = BlobURL.fromContainerURL(containerURL, file); + const blockBlobURL = BlockBlobURL.fromBlobURL(blobURL); + + const blobContentType = lookup(file) || ''; + const blobContentEncoding = charset(blobContentType) || ''; + + await uploadStreamToBlockBlob( + Aborter.timeout(30 * 60 * 60 * 1000), + fs.createReadStream(path.join(filesPath, file)), + blockBlobURL, + 4 * 1024 * 1024, + 20, + { + blobHTTPHeaders: { + blobContentType, + blobContentEncoding + } + } ); - bar.tick(0); + bar.tick(1); + }); - await promiseLimit(5).map(files, async function(file: string) { - const blobURL = BlobURL.fromContainerURL(containerURL, file); - const blockBlobURL = BlockBlobURL.fromBlobURL(blobURL); - - const blobContentType = lookup(file) || ''; - const blobContentEncoding = charset(blobContentType) || ''; - - await uploadStreamToBlockBlob( - Aborter.timeout(30 * 60 * 60 * 1000), - fs.createReadStream(path.join(filesPath, file)), - blockBlobURL, - 4 * 1024 * 1024, - 20, - { - blobHTTPHeaders: { - blobContentType, - blobContentEncoding - } - } - ); - - bar.tick(1); - }); - - bar.terminate(); - context.logger.info('deploying static site'); + bar.terminate(); + context.logger.info('deploying static site'); } diff --git a/src/builders/deploy.builder.ts b/src/builders/deploy.builder.ts index 4ab0ebb..9645683 100644 --- a/src/builders/deploy.builder.ts +++ b/src/builders/deploy.builder.ts @@ -11,33 +11,37 @@ import { AzureHostingConfig, AzureJSON } from '../util/workspace/azure-json'; import deploy from './actions/deploy'; export default createBuilder( - async (builderConfig: any, context: BuilderContext): Promise => { - const root = normalize(context.workspaceRoot); - const workspace = new experimental.workspace.Workspace(root, new NodeJsSyncHost()); - await workspace.loadWorkspaceFromHost(normalize('angular.json')).toPromise(); + async (builderConfig: any, context: BuilderContext): Promise => { + const root = normalize(context.workspaceRoot); + const workspace = new experimental.workspace.Workspace(root, new NodeJsSyncHost()); + await workspace.loadWorkspaceFromHost(normalize('angular.json')).toPromise(); - if (!context.target) { - throw new Error('Cannot deploy the application without a target'); - } - - const project = workspace.getProject(context.target.project); - const workspaceRoot = getSystemPath(workspace.root); - - const azureProject = getAzureHostingConfig(workspaceRoot, context.target.project, builderConfig.config); - - try { - await deploy(context, join(workspaceRoot, project.root), azureProject); - } catch (e) { - context.logger.error('Error when trying to deploy: '); - context.logger.error(e.message); - return { success: false }; - } - return { success: true }; + if (!context.target) { + throw new Error('Cannot deploy the application without a target'); } + + const project = workspace.getProject(context.target.project); + const workspaceRoot = getSystemPath(workspace.root); + + const azureProject = getAzureHostingConfig(workspaceRoot, context.target.project, builderConfig.config); + + try { + await deploy(context, join(workspaceRoot, project.root), azureProject); + } catch (e) { + context.logger.error('Error when trying to deploy: '); + context.logger.error(e.message); + return { success: false }; + } + return { success: true }; + } ); -export function getAzureHostingConfig(projectRoot: string, target: string, azureConfigFile: string): AzureHostingConfig | undefined { - const azureJson: AzureJSON = JSON.parse(readFileSync(join(projectRoot, azureConfigFile), 'UTF-8')); - const projects = azureJson.hosting; - return projects.find(project => project.app.project === target); +export function getAzureHostingConfig( + projectRoot: string, + target: string, + azureConfigFile: string +): AzureHostingConfig | undefined { + const azureJson: AzureJSON = JSON.parse(readFileSync(join(projectRoot, azureConfigFile), 'UTF-8')); + const projects = azureJson.hosting; + return projects.find(project => project.app.project === target); } diff --git a/src/builders/logout.builder.ts b/src/builders/logout.builder.ts index 7eb3788..ff07822 100644 --- a/src/builders/logout.builder.ts +++ b/src/builders/logout.builder.ts @@ -6,9 +6,9 @@ import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/ar import { clearCreds } from '../util/azure/auth'; export default createBuilder( - async (builderConfig: any, context: BuilderContext): Promise => { - await clearCreds(); - context.logger.info('Cleared Azure credentials from cache.'); - return { success: true }; - } + async (builderConfig: any, context: BuilderContext): Promise => { + await clearCreds(); + context.logger.info('Cleared Azure credentials from cache.'); + return { success: true }; + } ); diff --git a/src/ng-add/index.spec.ts b/src/ng-add/index.spec.ts index 7e1462f..82ce452 100644 --- a/src/ng-add/index.spec.ts +++ b/src/ng-add/index.spec.ts @@ -28,8 +28,12 @@ describe('ng add @azure/ng-deploy', () => { const testRunner = new SchematicTestRunner('schematics', collectionPath); async function initAngularProject(): Promise { - const appTree = await testRunner.runExternalSchematicAsync('@schematics/angular', 'workspace', workspaceOptions).toPromise(); - return await testRunner.runExternalSchematicAsync('@schematics/angular', 'application', appOptions, appTree).toPromise(); + const appTree = await testRunner + .runExternalSchematicAsync('@schematics/angular', 'workspace', workspaceOptions) + .toPromise(); + return await testRunner + .runExternalSchematicAsync('@schematics/angular', 'application', appOptions, appTree) + .toPromise(); } it('fails with a missing tree', async () => { @@ -38,7 +42,7 @@ describe('ng add @azure/ng-deploy', () => { it('adds azure deploy to an existing project', async () => { let appTree = await initAngularProject(); - appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise() + appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise(); const angularJson = JSON.parse(appTree.readContent('/angular.json')); expect(angularJson.projects[appOptions.name].architect.deploy).toBeDefined(); @@ -50,15 +54,15 @@ describe('ng add @azure/ng-deploy', () => { hosting: [ { app: { - configuration: "production", - path: "dist/test-app", - project: "test-app", - target: "build", + configuration: 'production', + path: 'dist/test-app', + project: 'test-app', + target: 'build' }, azureHosting: { - account: "fakeStorageAccount", - resourceGroupName: "fake-resource-group", - subscription: "fake-subscription-1234", + account: 'fakeStorageAccount', + resourceGroupName: 'fake-resource-group', + subscription: 'fake-subscription-1234' } } ] @@ -68,7 +72,7 @@ describe('ng add @azure/ng-deploy', () => { it('should overwrite existing hosting config', async () => { // Simulate existing app setup let appTree = await initAngularProject(); - appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise() + appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise(); appTree.overwrite('/azure.json', appTree.readContent('azure.json').replace(/fake/g, 'existing')); const confirmMock = confirm as jest.Mock; @@ -76,7 +80,7 @@ describe('ng add @azure/ng-deploy', () => { confirmMock.mockImplementationOnce(() => Promise.resolve(true)); // Run ng add @azure/deploy on existing project - appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise() + appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise(); expect(confirm).toHaveBeenCalledTimes(1); expect(appTree.files).toContain('/azure.json'); @@ -86,15 +90,15 @@ describe('ng add @azure/ng-deploy', () => { hosting: [ { app: { - configuration: "production", - path: "dist/test-app", - project: "test-app", - target: "build", + configuration: 'production', + path: 'dist/test-app', + project: 'test-app', + target: 'build' }, azureHosting: { - account: "fakeStorageAccount", - resourceGroupName: "fake-resource-group", - subscription: "fake-subscription-1234", + account: 'fakeStorageAccount', + resourceGroupName: 'fake-resource-group', + subscription: 'fake-subscription-1234' } } ] @@ -104,7 +108,7 @@ describe('ng add @azure/ng-deploy', () => { it('should keep existing hosting config', async () => { // Simulate existing app setup let appTree = await initAngularProject(); - appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise() + appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise(); appTree.overwrite('/azure.json', appTree.readContent('azure.json').replace(/fake/g, 'existing')); const confirmMock = confirm as jest.Mock; @@ -112,7 +116,7 @@ describe('ng add @azure/ng-deploy', () => { confirmMock.mockImplementationOnce(() => Promise.resolve(false)); // Run ng add @azure/deploy on existing project - appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise() + appTree = await testRunner.runSchematicAsync('ng-add', {}, appTree).toPromise(); expect(confirm).toHaveBeenCalledTimes(1); expect(appTree.files).toContain('/azure.json'); @@ -122,15 +126,15 @@ describe('ng add @azure/ng-deploy', () => { hosting: [ { app: { - configuration: "production", - path: "dist/test-app", - project: "test-app", - target: "build", + configuration: 'production', + path: 'dist/test-app', + project: 'test-app', + target: 'build' }, azureHosting: { - account: "existingStorageAccount", - resourceGroupName: "existing-resource-group", - subscription: "existing-subscription-1234", + account: 'existingStorageAccount', + resourceGroupName: 'existing-resource-group', + subscription: 'existing-subscription-1234' } } ] diff --git a/src/ng-add/index.ts b/src/ng-add/index.ts index 229690a..7aea5fb 100644 --- a/src/ng-add/index.ts +++ b/src/ng-add/index.ts @@ -14,47 +14,43 @@ import { generateAzureJson, readAzureJson, getAzureHostingConfig } from '../util import { AddOptions } from '../util/shared/types'; export function ngAdd(_options: AddOptions): Rule { - return (tree: Tree, _context: SchematicContext) => { - return chain([ - addDeployAzure(_options) - ])(tree, _context); - }; + return (tree: Tree, _context: SchematicContext) => { + return chain([addDeployAzure(_options)])(tree, _context); + }; } export function addDeployAzure(_options: AddOptions): Rule { - return async (tree: Tree, _context: SchematicContext) => { - const project = new AngularWorkspace(tree, _options); - const azureJson = readAzureJson(tree); - const hostingConfig = azureJson ? getAzureHostingConfig(azureJson, project.projectName) : null; + return async (tree: Tree, _context: SchematicContext) => { + const project = new AngularWorkspace(tree, _options); + const azureJson = readAzureJson(tree); + const hostingConfig = azureJson ? getAzureHostingConfig(azureJson, project.projectName) : null; - if (!hostingConfig || await confirm(`Overwrite existing Azure config for ${ project.projectName }?`)) { - - const auth = await loginToAzure(_context.logger); - const credentials = auth.credentials as DeviceTokenCredentials; - const subscription = await selectSubscription(auth.subscriptions, _options, _context.logger); - const resourceGroup = await getResourceGroup(credentials, subscription, _options, _context.logger); - const client = getAzureStorageClient(credentials, subscription); - const account = await getAccount(client, resourceGroup, _options, _context.logger); - - const appDeployConfig = { - project: project.projectName, - target: project.target, - configuration: project.configuration, - path: project.path - }; - - const azureDeployConfig = { - subscription, - resourceGroupName: resourceGroup.name, - account - }; - - // TODO: log url for account at Azure portal - generateAzureJson(tree, appDeployConfig, azureDeployConfig); - - } + if (!hostingConfig || (await confirm(`Overwrite existing Azure config for ${project.projectName}?`))) { + const auth = await loginToAzure(_context.logger); + const credentials = auth.credentials as DeviceTokenCredentials; + const subscription = await selectSubscription(auth.subscriptions, _options, _context.logger); + const resourceGroup = await getResourceGroup(credentials, subscription, _options, _context.logger); + const client = getAzureStorageClient(credentials, subscription); + const account = await getAccount(client, resourceGroup, _options, _context.logger); - project.addLogoutArchitect(); - project.addDeployArchitect(); - }; + const appDeployConfig = { + project: project.projectName, + target: project.target, + configuration: project.configuration, + path: project.path + }; + + const azureDeployConfig = { + subscription, + resourceGroupName: resourceGroup.name, + account + }; + + // TODO: log url for account at Azure portal + generateAzureJson(tree, appDeployConfig, azureDeployConfig); + } + + project.addLogoutArchitect(); + project.addDeployArchitect(); + }; } diff --git a/src/util/azure/__mocks__/auth.ts b/src/util/azure/__mocks__/auth.ts index d65c359..65f094f 100644 --- a/src/util/azure/__mocks__/auth.ts +++ b/src/util/azure/__mocks__/auth.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export const loginToAzure = () => Promise.resolve({ - credentials: null, - subscriptions: [] -}); +export const loginToAzure = () => + Promise.resolve({ + credentials: null, + subscriptions: [] + }); diff --git a/src/util/azure/__mocks__/resource-group-helper.ts b/src/util/azure/__mocks__/resource-group-helper.ts index 8ab5e5c..2ce0955 100644 --- a/src/util/azure/__mocks__/resource-group-helper.ts +++ b/src/util/azure/__mocks__/resource-group-helper.ts @@ -4,23 +4,23 @@ *--------------------------------------------------------------------------------------------*/ export async function getResourceGroups() { - return Promise.resolve([{ - id: '1', - name: 'mock', - location: 'location' - }, - { - id: '2', - name: 'mock2', - location: 'location' - }, - { - id: '3', - name: 'mock3', - location: 'location' - }]); + return Promise.resolve([ + { + id: '1', + name: 'mock', + location: 'location' + }, + { + id: '2', + name: 'mock2', + location: 'location' + }, + { + id: '3', + name: 'mock3', + location: 'location' + } + ]); } -export const createResourceGroup = jest.fn((name: string) => Promise.resolve({ name })) - - +export const createResourceGroup = jest.fn((name: string) => Promise.resolve({ name })); diff --git a/src/util/azure/__mocks__/resource-group.ts b/src/util/azure/__mocks__/resource-group.ts index 1287366..aa08425 100644 --- a/src/util/azure/__mocks__/resource-group.ts +++ b/src/util/azure/__mocks__/resource-group.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - export const getResourceGroup = () => Promise.resolve({ - id: '4321', - name: 'fake-resource-group', - location: 'westus' - }); +export const getResourceGroup = () => + Promise.resolve({ + id: '4321', + name: 'fake-resource-group', + location: 'westus' + }); diff --git a/src/util/azure/__mocks__/subscription.ts b/src/util/azure/__mocks__/subscription.ts index 08f4fab..8889f68 100644 --- a/src/util/azure/__mocks__/subscription.ts +++ b/src/util/azure/__mocks__/subscription.ts @@ -3,5 +3,4 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - export const selectSubscription = () => Promise.resolve('fake-subscription-1234'); - \ No newline at end of file +export const selectSubscription = () => Promise.resolve('fake-subscription-1234'); diff --git a/src/util/azure/__tests__/locations.ts b/src/util/azure/__tests__/locations.ts index 6b2e1e7..2eac794 100644 --- a/src/util/azure/__tests__/locations.ts +++ b/src/util/azure/__tests__/locations.ts @@ -15,4 +15,4 @@ describe('location', () => { expect(actual && actual.id).toBe('southafricanorth'); expect(actual && actual.name).toBe('South Africa North'); }); -}) +}); diff --git a/src/util/azure/__tests__/resource-group.ts b/src/util/azure/__tests__/resource-group.ts index 61e0362..f105c30 100644 --- a/src/util/azure/__tests__/resource-group.ts +++ b/src/util/azure/__tests__/resource-group.ts @@ -6,7 +6,6 @@ import { getResourceGroup, ResourceGroup } from '../resource-group'; import { DeviceTokenCredentials } from '@azure/ms-rest-nodeauth'; import { AddOptions } from '../../shared/types'; - const RESOURCE_GROUP = 'GROUP'; const credentials = {}; @@ -34,21 +33,23 @@ describe('resource group', () => { createResourceGroupMock.mockClear(); }); - test.only('should create resource group', async() => { + test.only('should create resource group', async () => { const subscription = ''; await getResourceGroup(credentials, subscription, options, logger); expect(createResourceGroupMock.mock.calls[0][0]).toBe(RESOURCE_GROUP); }); - test('should use existing resource group and return it', async() => { + test('should use existing resource group and return it', async () => { // there needs to be a match towards resource group list const subscription = ''; - const existingMockResourceGroup = 'mock2' - const optionsWithMatch = { ...options, resourceGroup: existingMockResourceGroup }; + const existingMockResourceGroup = 'mock2'; + const optionsWithMatch = { + ...options, + resourceGroup: existingMockResourceGroup + }; const resourceGroup: ResourceGroup = await getResourceGroup(credentials, subscription, optionsWithMatch, logger); - expect(createResourceGroupMock.mock.calls.length).toBe(0); expect(logger.info.mock.calls.length).toBe(1); diff --git a/src/util/azure/__tests__/subscription.ts b/src/util/azure/__tests__/subscription.ts index 4b0aa39..39f56ee 100644 --- a/src/util/azure/__tests__/subscription.ts +++ b/src/util/azure/__tests__/subscription.ts @@ -7,21 +7,20 @@ import { LinkedSubscription } from '@azure/ms-rest-nodeauth'; import { AddOptions } from '../../shared/types'; jest.mock('inquirer'); - -// AddOptions, Logger +// AddOptions, Logger const SUBID = '124'; const SUBNAME = 'name'; -const optionsMock = { +const optionsMock = { subscriptionId: SUBID, subscriptionName: SUBNAME }; // const optionsMockEmpty = {}; -const loggerMock = { +const loggerMock = { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), @@ -38,61 +37,59 @@ describe('subscription', () => { }); test('should throw error when input is an EMPTY array', async () => { - const errorMessage = 'You don\'t have any active subscriptions. ' + + const errorMessage = + "You don't have any active subscriptions. " + 'Head to https://azure.com/free and sign in. From there you can create a new subscription ' + 'and then you can come back and try again.'; - - expect(selectSubscription([], optionsMock, loggerMock)).rejects.toEqual(new Error(errorMessage)) + expect(selectSubscription([], optionsMock, loggerMock)).rejects.toEqual(new Error(errorMessage)); }); - test('provided sub id DOES NOT match when provided in options', async() => { - const subs = >[{ - id: '456', - name: 'a sub' - }]; - + test('provided sub id DOES NOT match when provided in options', async () => { + const subs = >[ + { + id: '456', + name: 'a sub' + } + ]; + selectSubscription(subs, optionsMock, loggerMock); const warnCalledTwice = loggerMock.warn.mock.calls.length === 2; expect(loggerMock.warn.mock.calls[0][0]).toBe(`The provided subscription ID does not exist.`); - expect(loggerMock.warn.mock.calls[1][0]).toBe(`Using subscription ${subs[0].name} - ${subs[0].id}`) + expect(loggerMock.warn.mock.calls[1][0]).toBe(`Using subscription ${subs[0].name} - ${subs[0].id}`); expect(warnCalledTwice).toBeTruthy(); }); test('should return first subscriptions id, if only ONE subscription', async () => { - const singleSubscription = { id: SUBID, name: SUBNAME }; + const singleSubscription = { id: SUBID, name: SUBNAME }; const subs = >[singleSubscription]; const actual = await selectSubscription(subs, optionsMock, loggerMock); const warnNotCalled = loggerMock.warn.mock.calls.length === 0; - + expect(warnNotCalled).toBeTruthy(); expect(actual).toEqual(singleSubscription.id); }); test('should throw error when input is undefined', async () => { - const errorMessage = 'API returned no subscription IDs. It should. ' + - 'Log in to https://portal.azure.com and see if there\'s something wrong with your account.'; - + const errorMessage = + 'API returned no subscription IDs. It should. ' + + "Log in to https://portal.azure.com and see if there's something wrong with your account."; // this one looks a bit weird because method is `async`, otherwise throwError() helper should be used - expect(selectSubscription(undefined, optionsMock, loggerMock)).rejects.toEqual(new Error(errorMessage)) + expect(selectSubscription(undefined, optionsMock, loggerMock)).rejects.toEqual(new Error(errorMessage)); }); test('should prompt user to select a subscription if more than one subscription', async () => { const expected = 'subMock'; // check inquirer.js at __mocks__ at root level - const subs = >[ - { id: 'abc', name: 'subMock' }, - { id: '123', name: 'sub2' } - ]; + const subs = >[{ id: 'abc', name: 'subMock' }, { id: '123', name: 'sub2' }]; const actual = await selectSubscription(subs, optionsMock, loggerMock); - + // TODO verify that prompt is being invoked expect(actual).toEqual(expected); }); }); - diff --git a/src/util/azure/account.ts b/src/util/azure/account.ts index 9429b63..6fef757 100644 --- a/src/util/azure/account.ts +++ b/src/util/azure/account.ts @@ -13,188 +13,168 @@ import { generateName } from '../prompt/name-generator'; import { spinner } from '../prompt/spinner'; interface AccountDetails extends ListItem { - id: string; - name: string; - location: string; + id: string; + name: string; + location: string; } const accountPromptOptions = { - id: 'account', - message: 'Under which storage account should we put this static site?' + id: 'account', + message: 'Under which storage account should we put this static site?' }; const newAccountPromptOptions = { - id: 'newAccount', - message: 'Enter a name for the new storage account:', - name: 'Create a new storage account', - default: '', - defaultGenerator: (name: string) => Promise.resolve(''), - validate: (name: string) => Promise.resolve(true) + id: 'newAccount', + message: 'Enter a name for the new storage account:', + name: 'Create a new storage account', + default: '', + defaultGenerator: (name: string) => Promise.resolve(''), + validate: (name: string) => Promise.resolve(true) }; export function getAzureStorageClient(credentials: DeviceTokenCredentials, subscriptionId: string) { - return new StorageManagementClient(credentials, subscriptionId); + return new StorageManagementClient(credentials, subscriptionId); } export async function getAccount( - client: StorageManagementClient, - resourceGroup: ResourceGroup, - options: AddOptions, - logger: Logger) { + client: StorageManagementClient, + resourceGroup: ResourceGroup, + options: AddOptions, + logger: Logger +) { + let accountName = options.account || ''; + let needToCreateAccount = false; - let accountName = options.account || ''; - let needToCreateAccount = false; + spinner.start('Fetching storage accounts'); + const accounts = await client.storageAccounts.listByResourceGroup(resourceGroup.name); + spinner.stop(); - spinner.start('Fetching storage accounts'); - const accounts = await client.storageAccounts.listByResourceGroup(resourceGroup.name); - spinner.stop(); + function getInitialAccountName() { + const normalizedProjectNameArray = options.project.match(/[a-zA-Z0-9]/g); + const normalizedProjectName = normalizedProjectNameArray ? normalizedProjectNameArray.join('') : ''; + return `${normalizedProjectName}static`; + } - function getInitialAccountName() { - const normalizedProjectNameArray = options.project.match(/[a-zA-Z0-9]/g); - const normalizedProjectName = normalizedProjectNameArray ? normalizedProjectNameArray.join('') : ''; - return `${ normalizedProjectName }static`; + const initialName = getInitialAccountName(); + const generateDefaultAccountName = accountNameGenerator(client); + const validateAccountName = checkNameAvailability(client, true); + + newAccountPromptOptions.default = initialName; + newAccountPromptOptions.defaultGenerator = generateDefaultAccountName; + newAccountPromptOptions.validate = validateAccountName; + + if (accountName) { + const account = accounts.find(acc => acc.name === accountName); + if (!!account) { + // account exists + // TODO: check account configuration + logger.info(`Using existing account ${accountName}`); + } else { + // create account with this name, if valid + const valid = await validateAccountName(accountName); + if (!valid) { + accountName = (await newItemPrompt(newAccountPromptOptions)).newAccount; + } + needToCreateAccount = true; } + } else { + // no account flag - const initialName = getInitialAccountName(); - const generateDefaultAccountName = accountNameGenerator(client); - const validateAccountName = checkNameAvailability(client, true); - - newAccountPromptOptions.default = initialName; - newAccountPromptOptions.defaultGenerator = generateDefaultAccountName; - newAccountPromptOptions.validate = validateAccountName; - - - if (accountName) { - const account = accounts.find(acc => acc.name === accountName); - if (!!account) { // account exists - // TODO: check account configuration - logger.info(`Using existing account ${ accountName }`); - - } else { // create account with this name, if valid - const valid = await validateAccountName(accountName); - if (!valid) { - accountName = (await newItemPrompt(newAccountPromptOptions)).newAccount; - } - needToCreateAccount = true; - } - } else { // no account flag - - if (!options.manual) { // quickstart - create w/ default name - accountName = await generateDefaultAccountName(initialName); - needToCreateAccount = true; - - } else { // select from list or create new - const result = await filteredList(accounts as AccountDetails[], accountPromptOptions, newAccountPromptOptions); - needToCreateAccount = !!result.newAccount; - accountName = result.newAccount || result.account.name; - } + if (!options.manual) { + // quickstart - create w/ default name + accountName = await generateDefaultAccountName(initialName); + needToCreateAccount = true; + } else { + // select from list or create new + const result = await filteredList(accounts as AccountDetails[], accountPromptOptions, newAccountPromptOptions); + needToCreateAccount = !!result.newAccount; + accountName = result.newAccount || result.account.name; } + } - if (needToCreateAccount) { - spinner.start(`creating ${ accountName }`); - await createAccount(accountName, client, resourceGroup.name, resourceGroup.location); - spinner.succeed(); - } + if (needToCreateAccount) { + spinner.start(`creating ${accountName}`); + await createAccount(accountName, client, resourceGroup.name, resourceGroup.location); + spinner.succeed(); + } - return accountName; + return accountName; } function checkNameAvailability(client: StorageManagementClient, warn?: boolean) { - return async (account: string) => { - spinner.start(); - const availability = await client.storageAccounts.checkNameAvailability(account); - if (!availability.nameAvailable && warn) { - spinner.fail(availability.message || 'chosen name is not available'); - } else { - spinner.stop(); - } - return !!availability.nameAvailable; - }; + return async (account: string) => { + spinner.start(); + const availability = await client.storageAccounts.checkNameAvailability(account); + if (!availability.nameAvailable && warn) { + spinner.fail(availability.message || 'chosen name is not available'); + } else { + spinner.stop(); + } + return !!availability.nameAvailable; + }; } function accountNameGenerator(client: StorageManagementClient) { - return async (name: string) => { - return await generateName(name, checkNameAvailability(client, false)); - }; + return async (name: string) => { + return await generateName(name, checkNameAvailability(client, false)); + }; } export async function setStaticSiteToPublic(serviceURL: ServiceURL) { - await serviceURL.setProperties(Aborter.timeout(30 * 60 * 60 * 1000), { - staticWebsite: { - enabled: true, - indexDocument: 'index.html', - errorDocument404Path: 'index.html' - } - }); + await serviceURL.setProperties(Aborter.timeout(30 * 60 * 60 * 1000), { + staticWebsite: { + enabled: true, + indexDocument: 'index.html', + errorDocument404Path: 'index.html' + } + }); } -export async function getAccountKey( - account: any, - client: StorageManagementClient, - resourceGroup: any -) { - const accountKeysRes = await client.storageAccounts.listKeys( - resourceGroup, - account - ); - const accountKey = (accountKeysRes.keys || []).filter( - key => (key.permissions || '').toUpperCase() === 'FULL' - )[0]; - if (!accountKey || !accountKey.value) { - process.exit(1); - return ''; - } - return accountKey.value; +export async function getAccountKey(account: any, client: StorageManagementClient, resourceGroup: any) { + const accountKeysRes = await client.storageAccounts.listKeys(resourceGroup, account); + const accountKey = (accountKeysRes.keys || []).filter(key => (key.permissions || '').toUpperCase() === 'FULL')[0]; + if (!accountKey || !accountKey.value) { + process.exit(1); + return ''; + } + return accountKey.value; } export async function createAccount( - account: string, - client: StorageManagementClient, - resourceGroupName: string, - location: string + account: string, + client: StorageManagementClient, + resourceGroupName: string, + location: string ) { - const poller = await client.storageAccounts.beginCreate( - resourceGroupName, - account, - { - kind: 'StorageV2', - location, - sku: { name: 'Standard_LRS' } - } - ); - await poller.pollUntilFinished(); + const poller = await client.storageAccounts.beginCreate(resourceGroupName, account, { + kind: 'StorageV2', + location, + sku: { name: 'Standard_LRS' } + }); + await poller.pollUntilFinished(); - spinner.start('Retrieving account keys'); - const accountKey = await getAccountKey(account, client, resourceGroupName); - if (!accountKey) { - throw new SchematicsException('no keys retrieved for storage account'); + spinner.start('Retrieving account keys'); + const accountKey = await getAccountKey(account, client, resourceGroupName); + if (!accountKey) { + throw new SchematicsException('no keys retrieved for storage account'); + } + spinner.succeed(); + + spinner.start('Creating web container'); + await createWebContainer(client, resourceGroupName, account); + spinner.succeed(); + const pipeline = ServiceURL.newPipeline(new SharedKeyCredential(account, accountKey)); + const serviceURL = new ServiceURL(`https://${account}.blob.core.windows.net`, pipeline); + spinner.start('Setting container to be publicly available static site'); + await setStaticSiteToPublic(serviceURL); + spinner.succeed(); +} + +export async function createWebContainer(client: StorageManagementClient, resourceGroup: any, account: any) { + await client.blobContainers.create(resourceGroup, account, '$web', { + publicAccess: 'Container', + metadata: { + cli: 'ng-deploy-azure' } - spinner.succeed(); - - spinner.start('Creating web container'); - await createWebContainer(client, resourceGroupName, account); - spinner.succeed(); - const pipeline = ServiceURL.newPipeline( - new SharedKeyCredential(account, accountKey) - ); - const serviceURL = new ServiceURL( - `https://${ account }.blob.core.windows.net`, - pipeline - ); - spinner.start('Setting container to be publicly available static site'); - await setStaticSiteToPublic(serviceURL); - spinner.succeed(); -} - -export async function createWebContainer( - client: StorageManagementClient, - resourceGroup: any, - account: any -) { - await client.blobContainers.create(resourceGroup, account, '$web', { - publicAccess: 'Container', - metadata: { - cli: 'ng-deploy-azure' - } - }); + }); } diff --git a/src/util/azure/auth.ts b/src/util/azure/auth.ts index ec08345..6d3c606 100644 --- a/src/util/azure/auth.ts +++ b/src/util/azure/auth.ts @@ -2,11 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { - interactiveLoginWithAuthResponse, - DeviceTokenCredentials, - AuthResponse -} from '@azure/ms-rest-nodeauth'; +import { interactiveLoginWithAuthResponse, DeviceTokenCredentials, AuthResponse } from '@azure/ms-rest-nodeauth'; import { MemoryCache } from 'adal-node'; import { Environment } from '@azure/ms-rest-azure-env'; import Conf from 'conf'; @@ -15,47 +11,46 @@ import { Logger } from '../shared/types'; const AUTH = 'auth'; export const globalConfig = new Conf({ - defaults: { - auth: null - }, - configName: 'ng-azure' + defaults: { + auth: null + }, + configName: 'ng-azure' }); export async function clearCreds() { - return globalConfig.set(AUTH, null); + return globalConfig.set(AUTH, null); } export async function loginToAzure(logger: Logger): Promise { - let auth = await globalConfig.get(AUTH) as AuthResponse | null; + let auth = (await globalConfig.get(AUTH)) as AuthResponse | null; - if (auth && auth.credentials) { + if (auth && auth.credentials) { + const creds = auth.credentials as DeviceTokenCredentials; + const cache = new MemoryCache(); + cache.add(creds.tokenCache._entries, () => {}); - const creds = auth.credentials as DeviceTokenCredentials; - const cache = new MemoryCache(); - cache.add(creds.tokenCache._entries, () => { - }); + auth.credentials = new DeviceTokenCredentials( + creds.clientId, + creds.domain, + creds.username, + creds.tokenAudience, + new Environment(creds.environment), + cache + ); - auth.credentials = new DeviceTokenCredentials( - creds.clientId, - creds.domain, - creds.username, - creds.tokenAudience, - new Environment(creds.environment), - cache); + const token = await auth.credentials.getToken(); + if (new Date(token.expiresOn).getTime() < Date.now()) { + logger.info(`Your stored credentials have expired; you'll have to log in again`); - const token = await auth.credentials.getToken(); - if (new Date(token.expiresOn).getTime() < Date.now()) { - logger.info(`Your stored credentials have expired; you'll have to log in again`); - - auth = await interactiveLoginWithAuthResponse(); - auth.credentials = auth.credentials as DeviceTokenCredentials; - globalConfig.set(AUTH, auth); - } - } else { - // user has to log in again - auth = await interactiveLoginWithAuthResponse(); - globalConfig.set(AUTH, auth); + auth = await interactiveLoginWithAuthResponse(); + auth.credentials = auth.credentials as DeviceTokenCredentials; + globalConfig.set(AUTH, auth); } + } else { + // user has to log in again + auth = await interactiveLoginWithAuthResponse(); + globalConfig.set(AUTH, auth); + } - return auth; + return auth; } diff --git a/src/util/azure/locations.ts b/src/util/azure/locations.ts index e5a41f8..17287bb 100644 --- a/src/util/azure/locations.ts +++ b/src/util/azure/locations.ts @@ -3,135 +3,135 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ export interface StorageLocation { - id: string; - name: string; + id: string; + name: string; } -export const defaultLocation = { - 'id': 'westus', - 'name': 'West US' +export const defaultLocation = { + id: 'westus', + name: 'West US' }; export const locations = [ - { - 'id': 'eastasia', - 'name': 'East Asia' - }, - { - 'id': 'southeastasia', - 'name': 'Southeast Asia' - }, - { - 'id': 'centralus', - 'name': 'Central US' - }, - { - 'id': 'eastus', - 'name': 'East US' - }, - { - 'id': 'eastus2', - 'name': 'East US 2' - }, - { - 'id': 'westus', - 'name': 'West US' - }, - { - 'id': 'northcentralus', - 'name': 'North Central US' - }, - { - 'id': 'southcentralus', - 'name': 'South Central US' - }, - { - 'id': 'northeurope', - 'name': 'North Europe' - }, - { - 'id': 'westeurope', - 'name': 'West Europe' - }, - { - 'id': 'japanwest', - 'name': 'Japan West' - }, - { - 'id': 'japaneast', - 'name': 'Japan East' - }, - { - 'id': 'brazilsouth', - 'name': 'Brazil South' - }, - { - 'id': 'australiaeast', - 'name': 'Australia East' - }, - { - 'id': 'australiasoutheast', - 'name': 'Australia Southeast' - }, - { - 'id': 'southindia', - 'name': 'South India' - }, - { - 'id': 'centralindia', - 'name': 'Central India' - }, - { - 'id': 'westindia', - 'name': 'West India' - }, - { - 'id': 'canadacentral', - 'name': 'Canada Central' - }, - { - 'id': 'canadaeast', - 'name': 'Canada East' - }, - { - 'id': 'uksouth', - 'name': 'UK South' - }, - { - 'id': 'ukwest', - 'name': 'UK West' - }, - { - 'id': 'westcentralus', - 'name': 'West Central US' - }, - { - 'id': 'westus2', - 'name': 'West US 2' - }, - { - 'id': 'koreacentral', - 'name': 'Korea Central' - }, - { - 'id': 'koreasouth', - 'name': 'Korea South' - }, - { - 'id': 'francecentral', - 'name': 'France Central' - }, - { - 'id': 'southafricanorth', - 'name': 'South Africa North' - } + { + id: 'eastasia', + name: 'East Asia' + }, + { + id: 'southeastasia', + name: 'Southeast Asia' + }, + { + id: 'centralus', + name: 'Central US' + }, + { + id: 'eastus', + name: 'East US' + }, + { + id: 'eastus2', + name: 'East US 2' + }, + { + id: 'westus', + name: 'West US' + }, + { + id: 'northcentralus', + name: 'North Central US' + }, + { + id: 'southcentralus', + name: 'South Central US' + }, + { + id: 'northeurope', + name: 'North Europe' + }, + { + id: 'westeurope', + name: 'West Europe' + }, + { + id: 'japanwest', + name: 'Japan West' + }, + { + id: 'japaneast', + name: 'Japan East' + }, + { + id: 'brazilsouth', + name: 'Brazil South' + }, + { + id: 'australiaeast', + name: 'Australia East' + }, + { + id: 'australiasoutheast', + name: 'Australia Southeast' + }, + { + id: 'southindia', + name: 'South India' + }, + { + id: 'centralindia', + name: 'Central India' + }, + { + id: 'westindia', + name: 'West India' + }, + { + id: 'canadacentral', + name: 'Canada Central' + }, + { + id: 'canadaeast', + name: 'Canada East' + }, + { + id: 'uksouth', + name: 'UK South' + }, + { + id: 'ukwest', + name: 'UK West' + }, + { + id: 'westcentralus', + name: 'West Central US' + }, + { + id: 'westus2', + name: 'West US 2' + }, + { + id: 'koreacentral', + name: 'Korea Central' + }, + { + id: 'koreasouth', + name: 'Korea South' + }, + { + id: 'francecentral', + name: 'France Central' + }, + { + id: 'southafricanorth', + name: 'South Africa North' + } ]; export function getLocation(locationName: string | undefined) { - if (!locationName) { - return; - } - return locations.find(location => { - return location.id === locationName || location.name === locationName; - }); + if (!locationName) { + return; + } + return locations.find(location => { + return location.id === locationName || location.name === locationName; + }); } diff --git a/src/util/azure/resource-group-helper.ts b/src/util/azure/resource-group-helper.ts index 604890b..d7056fb 100644 --- a/src/util/azure/resource-group-helper.ts +++ b/src/util/azure/resource-group-helper.ts @@ -2,10 +2,10 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ResourceManagementClient } from "@azure/arm-resources"; -import { ListItem } from "../prompt/list"; -import { DeviceTokenCredentials } from "@azure/ms-rest-nodeauth"; -import { ResourceGroupsCreateOrUpdateResponse } from "@azure/arm-resources/esm/models"; +import { ResourceManagementClient } from '@azure/arm-resources'; +import { ListItem } from '../prompt/list'; +import { DeviceTokenCredentials } from '@azure/ms-rest-nodeauth'; +import { ResourceGroupsCreateOrUpdateResponse } from '@azure/arm-resources/esm/models'; export interface ResourceGroupDetails extends ListItem { id: string; @@ -16,7 +16,7 @@ export interface ResourceGroupDetails extends ListItem { export async function getResourceGroups(creds: DeviceTokenCredentials, subscription: string) { const client = new ResourceManagementClient(creds, subscription); - const resourceGroupList = await client.resourceGroups.list() as ResourceGroupDetails[]; + const resourceGroupList = (await client.resourceGroups.list()) as ResourceGroupDetails[]; return resourceGroupList; } @@ -28,7 +28,8 @@ export async function createResourceGroup( ): Promise { // TODO: throws an error here if the subscription is wrong const client = new ResourceManagementClient(creds, subscription); - const resourceGroupRes = await client.resourceGroups.createOrUpdate(name, { location }); + const resourceGroupRes = await client.resourceGroups.createOrUpdate(name, { + location + }); return resourceGroupRes; } - diff --git a/src/util/azure/resource-group.ts b/src/util/azure/resource-group.ts index 5211d8f..fb86836 100644 --- a/src/util/azure/resource-group.ts +++ b/src/util/azure/resource-group.ts @@ -11,98 +11,100 @@ import { getResourceGroups, ResourceGroupDetails, createResourceGroup } from './ import { spinner } from '../prompt/spinner'; const defaultLocation = { - id: 'westus', - name: 'West US' + id: 'westus', + name: 'West US' }; export interface ResourceGroup { - id: string; - name: string; - location: string; + id: string; + name: string; + location: string; } const resourceGroupsPromptOptions = { - id: 'resourceGroup', - message: 'Under which resource group should we put this static site?' + id: 'resourceGroup', + message: 'Under which resource group should we put this static site?' }; const newResourceGroupsPromptOptions = { - id: 'newResourceGroup', - message: 'Enter a name for the new resource group:', - name: 'Create a new resource group', - default: '' + id: 'newResourceGroup', + message: 'Enter a name for the new resource group:', + name: 'Create a new resource group', + default: '' }; const locationPromptOptions = { - id: 'location', - message: 'In which location should the storage account be created?' + id: 'location', + message: 'In which location should the storage account be created?' }; export async function getResourceGroup( - creds: DeviceTokenCredentials, subscription: string, options: AddOptions, logger: Logger + creds: DeviceTokenCredentials, + subscription: string, + options: AddOptions, + logger: Logger ): Promise { + let resourceGroupName = options.resourceGroup || ''; + let location = getLocation(options.location); - let resourceGroupName = options.resourceGroup || ''; - let location = getLocation(options.location); + spinner.start('Fetching resource groups'); + const resourceGroupList = await getResourceGroups(creds, subscription); + spinner.stop(); + let result; - spinner.start('Fetching resource groups'); - const resourceGroupList = await getResourceGroups(creds, subscription); - spinner.stop(); - let result; + const initialName = options.project + '-static-deploy'; + const defaultResourceGroupName = await resourceGroupNameGenerator(initialName, resourceGroupList); - const initialName = options.project + '-static-deploy'; - const defaultResourceGroupName = await resourceGroupNameGenerator(initialName, resourceGroupList); + if (!options.manual) { + // quickstart + resourceGroupName = resourceGroupName || defaultResourceGroupName; + location = location || defaultLocation; + } - if (!options.manual) { // quickstart - resourceGroupName = resourceGroupName || defaultResourceGroupName; - location = location || defaultLocation; + if (!!resourceGroupName) { + // provided or quickstart + default + result = resourceGroupList.find(rg => rg.name === resourceGroupName); + if (!!result) { + logger.info(`Using existing resource group ${resourceGroupName}`); } + } else { + // not provided + manual - if (!!resourceGroupName) { // provided or quickstart + default - result = resourceGroupList.find(rg => rg.name === resourceGroupName); - if (!!result) { - logger.info(`Using existing resource group ${ resourceGroupName }`); - } - } else { // not provided + manual + // TODO: default name can be assigned later, only if creating a new resource group. + // TODO: check availability of the default name + newResourceGroupsPromptOptions.default = defaultResourceGroupName; - // TODO: default name can be assigned later, only if creating a new resource group. - // TODO: check availability of the default name - newResourceGroupsPromptOptions.default = defaultResourceGroupName; + result = await filteredList(resourceGroupList, resourceGroupsPromptOptions, newResourceGroupsPromptOptions); - result = (await filteredList( - resourceGroupList, - resourceGroupsPromptOptions, - newResourceGroupsPromptOptions)); + // TODO: add check whether the new resource group doesn't already exist. + // Currently throws an error of exists in a different location: + // Invalid resource group location 'westus'. The Resource group already exists in location 'eastus2'. - // TODO: add check whether the new resource group doesn't already exist. - // Currently throws an error of exists in a different location: - // Invalid resource group location 'westus'. The Resource group already exists in location 'eastus2'. + result = result.resourceGroup || result; + resourceGroupName = result.newResourceGroup || result.name; + } - result = result.resourceGroup || result; - resourceGroupName = result.newResourceGroup || result.name; - } + if (!result || result.newResourceGroup) { + location = location || (await askLocation()); // if quickstart - location defined above + spinner.start(`Creating resource group ${resourceGroupName} at ${location.name} (${location.id})`); + result = await createResourceGroup(resourceGroupName, subscription, creds, location.id); + spinner.succeed(); + } - if (!result || result.newResourceGroup) { - location = location || await askLocation(); // if quickstart - location defined above - spinner.start(`Creating resource group ${ resourceGroupName } at ${ location.name } (${ location.id })`); - result = await createResourceGroup(resourceGroupName, subscription, creds, location.id); - spinner.succeed(); - } - - return result; + return result; } export async function askLocation(): Promise { - const res = await filteredList(locations, locationPromptOptions); - return res.location; + const res = await filteredList(locations, locationPromptOptions); + return res.location; } function resourceGroupExists(resourceGroupList: ResourceGroupDetails[]) { - return async (name: string) => { - return Promise.resolve(!resourceGroupList.find(rg => rg.name === name)); - }; + return async (name: string) => { + return Promise.resolve(!resourceGroupList.find(rg => rg.name === name)); + }; } async function resourceGroupNameGenerator(initialName: string, resourceGroupList: ResourceGroupDetails[]) { - return await generateName(initialName, resourceGroupExists(resourceGroupList)); + return await generateName(initialName, resourceGroupExists(resourceGroupList)); } diff --git a/src/util/azure/subscription.ts b/src/util/azure/subscription.ts index 0d0a8de..32dd3c1 100644 --- a/src/util/azure/subscription.ts +++ b/src/util/azure/subscription.ts @@ -6,56 +6,55 @@ import { LinkedSubscription } from '@azure/ms-rest-nodeauth'; import { prompt } from 'inquirer'; import { AddOptions, Logger } from '../shared/types'; - export async function selectSubscription( - subs: LinkedSubscription[] | undefined, - options: AddOptions, - logger: Logger + subs: LinkedSubscription[] | undefined, + options: AddOptions, + logger: Logger ): Promise { - if (Array.isArray(subs)) { - if (subs.length === 0) { - throw new Error( - 'You don\'t have any active subscriptions. ' + - 'Head to https://azure.com/free and sign in. From there you can create a new subscription ' + - 'and then you can come back and try again.' - ); - } - - const subProvided = !!options.subscriptionId || !!options.subscriptionName; - const foundSub = subs.find(sub => { - // TODO: provided id and name might be of different subscriptions or one with typo - return sub.id === options.subscriptionId || sub.name === options.subscriptionName; - }); - - if (foundSub) { - return foundSub.id; - } else if (subProvided) { - logger.warn(`The provided subscription ID does not exist.`); - } - - if (subs.length === 1) { - if (subProvided) { - logger.warn(`Using subscription ${ subs[0].name } - ${ subs[0].id }`); - } - return subs[0].id; - } else { - const { sub } = await prompt<{ sub: any }>([ - { - type: 'list', - name: 'sub', - choices: subs.map(choice => ({ - name: `${ choice.name } – ${ choice.id }`, - value: choice.id - })), - message: 'Under which subscription should we put this static site?' - } - ]); - return sub; - } + if (Array.isArray(subs)) { + if (subs.length === 0) { + throw new Error( + "You don't have any active subscriptions. " + + 'Head to https://azure.com/free and sign in. From there you can create a new subscription ' + + 'and then you can come back and try again.' + ); } - throw new Error( - 'API returned no subscription IDs. It should. ' + - 'Log in to https://portal.azure.com and see if there\'s something wrong with your account.' - ); + const subProvided = !!options.subscriptionId || !!options.subscriptionName; + const foundSub = subs.find(sub => { + // TODO: provided id and name might be of different subscriptions or one with typo + return sub.id === options.subscriptionId || sub.name === options.subscriptionName; + }); + + if (foundSub) { + return foundSub.id; + } else if (subProvided) { + logger.warn(`The provided subscription ID does not exist.`); + } + + if (subs.length === 1) { + if (subProvided) { + logger.warn(`Using subscription ${subs[0].name} - ${subs[0].id}`); + } + return subs[0].id; + } else { + const { sub } = await prompt<{ sub: any }>([ + { + type: 'list', + name: 'sub', + choices: subs.map(choice => ({ + name: `${choice.name} – ${choice.id}`, + value: choice.id + })), + message: 'Under which subscription should we put this static site?' + } + ]); + return sub; + } + } + + throw new Error( + 'API returned no subscription IDs. It should. ' + + "Log in to https://portal.azure.com and see if there's something wrong with your account." + ); } diff --git a/src/util/prompt/__mocks__/spinner.ts b/src/util/prompt/__mocks__/spinner.ts index 9ab57a8..da15d7b 100644 --- a/src/util/prompt/__mocks__/spinner.ts +++ b/src/util/prompt/__mocks__/spinner.ts @@ -3,5 +3,4 @@ export const spinner = { start: jest.fn(), stop: jest.fn(), succeed: jest.fn() -} - +}; diff --git a/src/util/prompt/list.ts b/src/util/prompt/list.ts index 756975f..5ef619a 100644 --- a/src/util/prompt/list.ts +++ b/src/util/prompt/list.ts @@ -7,89 +7,89 @@ import * as inquirer from 'inquirer'; const fuzzy = require('fuzzy'); export interface PromptOptions { - name?: string; - message: string; - default?: string; - defaultGenerator?: ((name: string) => Promise); - title?: string; - validate?: any; - id: string; + name?: string; + message: string; + default?: string; + defaultGenerator?: (name: string) => Promise; + title?: string; + validate?: any; + id: string; } export interface ListItem { - name: string; // display name - id?: string; + name: string; // display name + id?: string; } inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')); export async function filteredList(list: ListItem[], listOptions: PromptOptions, newItemOptions?: PromptOptions) { - if (!list || list.length === 0) { - return newItemOptions && newItemPrompt(newItemOptions); - } + if (!list || list.length === 0) { + return newItemOptions && newItemPrompt(newItemOptions); + } - const displayedList = newItemOptions ? [newItemOptions, ...list] : list; - const result = await listPrompt(displayedList as ListItem[], listOptions.id, listOptions.message); + const displayedList = newItemOptions ? [newItemOptions, ...list] : list; + const result = await listPrompt(displayedList as ListItem[], listOptions.id, listOptions.message); - if (newItemOptions && newItemOptions.id && result[listOptions.id].id === newItemOptions.id) { - return newItemPrompt(newItemOptions); - } - return result; + if (newItemOptions && newItemOptions.id && result[listOptions.id].id === newItemOptions.id) { + return newItemPrompt(newItemOptions); + } + return result; } export async function newItemPrompt(newItemOptions: PromptOptions) { - let item, valid = true; - const defaultValue = - newItemOptions.defaultGenerator ? - await newItemOptions.defaultGenerator(newItemOptions.default || '') : - newItemOptions.default; - do { - item = await (inquirer as any).prompt({ - type: 'input', - name: newItemOptions.id, - default: defaultValue, - message: newItemOptions.message - }); + let item, + valid = true; + const defaultValue = newItemOptions.defaultGenerator + ? await newItemOptions.defaultGenerator(newItemOptions.default || '') + : newItemOptions.default; + do { + item = await (inquirer as any).prompt({ + type: 'input', + name: newItemOptions.id, + default: defaultValue, + message: newItemOptions.message + }); - if (newItemOptions.validate) { - valid = await newItemOptions.validate(item[newItemOptions.id]); - } - } while (!valid); + if (newItemOptions.validate) { + valid = await newItemOptions.validate(item[newItemOptions.id]); + } + } while (!valid); - return item; + return item; } export function listPrompt(list: ListItem[], name: string, message: string) { - return (inquirer as any).prompt({ - type: 'autocomplete', - name, - source: searchList(list), - message - }); + return (inquirer as any).prompt({ + type: 'autocomplete', + name, + source: searchList(list), + message + }); } const isListItem = (elem: ListItem | { original: ListItem }): elem is ListItem => { - return (<{ original: ListItem }>elem).original === undefined; + return (<{ original: ListItem }>elem).original === undefined; }; function searchList(list: ListItem[]) { - return (_: any, input: string) => { - return Promise.resolve( - fuzzy - .filter(input, list, { - extract(el: ListItem) { - return el.name; - } - }) - .map((result: ListItem | { original: ListItem }) => { - let original: ListItem; - if (isListItem(result)) { - original = result; - } else { - original = result.original; - } - return { name: original.name, value: original }; - }) - ); - }; + return (_: any, input: string) => { + return Promise.resolve( + fuzzy + .filter(input, list, { + extract(el: ListItem) { + return el.name; + } + }) + .map((result: ListItem | { original: ListItem }) => { + let original: ListItem; + if (isListItem(result)) { + original = result; + } else { + original = result.original; + } + return { name: original.name, value: original }; + }) + ); + }; } diff --git a/src/util/prompt/name-generator.ts b/src/util/prompt/name-generator.ts index 63167fc..7cf5a60 100644 --- a/src/util/prompt/name-generator.ts +++ b/src/util/prompt/name-generator.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ export async function generateName(name: string, validate: (name: string) => Promise) { - let valid = false; - do { - valid = await validate(name); - if (!valid) { - name = `${ name }${ Math.ceil(Math.random() * 100) }`; - } - } while (!valid); - return name; + let valid = false; + do { + valid = await validate(name); + if (!valid) { + name = `${name}${Math.ceil(Math.random() * 100)}`; + } + } while (!valid); + return name; } diff --git a/src/util/prompt/spinner.ts b/src/util/prompt/spinner.ts index c2ec789..990dd67 100644 --- a/src/util/prompt/spinner.ts +++ b/src/util/prompt/spinner.ts @@ -6,35 +6,27 @@ import chalk from 'chalk'; const ora = require('ora'); export const spinner = ora({ - text: 'Rounding up all the reptiles', - spinner: { - frames: [ - chalk.red('▌'), - chalk.green('▀'), - chalk.yellow('▐'), - chalk.blue('▄') - ], - interval: 100 - }, + text: 'Rounding up all the reptiles', + spinner: { + frames: [chalk.red('▌'), chalk.green('▀'), chalk.yellow('▐'), chalk.blue('▄')], + interval: 100 + } }); - -export function spin (msg?: string) { - return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { - const originalMethod = descriptor.value; - descriptor.value = async function () { - spinner.start(msg); - let result; - try { - result = await originalMethod.apply(this, arguments); - } catch (e) { - spinner.fail(e); - } - spinner.succeed(); - return result; - }; - return descriptor; +export function spin(msg?: string) { + return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + descriptor.value = async function() { + spinner.start(msg); + let result; + try { + result = await originalMethod.apply(this, arguments); + } catch (e) { + spinner.fail(e); + } + spinner.succeed(); + return result; }; - + return descriptor; + }; } - diff --git a/src/util/shared/types.ts b/src/util/shared/types.ts index 41ebc7c..113194a 100644 --- a/src/util/shared/types.ts +++ b/src/util/shared/types.ts @@ -5,24 +5,24 @@ import { JsonObject } from '@angular-devkit/core'; export interface Logger { - debug(message: string, metadata?: JsonObject): void; - info(message: string, metadata?: JsonObject): void; - warn(message: string, metadata?: JsonObject): void; - error(message: string, metadata?: JsonObject): void; - fatal(message: string, metadata?: JsonObject): void; + debug(message: string, metadata?: JsonObject): void; + info(message: string, metadata?: JsonObject): void; + warn(message: string, metadata?: JsonObject): void; + error(message: string, metadata?: JsonObject): void; + fatal(message: string, metadata?: JsonObject): void; } export interface AddOptions { - project: string; - manual?: boolean; - subscriptionId?: string; - subscriptionName?: string; - resourceGroup?: string; - account?: string; - location?: string; - 'resource-allocation'?: boolean; - config?: boolean; - dry?: boolean; - telemetry?: boolean; - '--'?: string[]; + project: string; + manual?: boolean; + subscriptionId?: string; + subscriptionName?: string; + resourceGroup?: string; + account?: string; + location?: string; + 'resource-allocation'?: boolean; + config?: boolean; + dry?: boolean; + telemetry?: boolean; + '--'?: string[]; } diff --git a/src/util/workspace/angular-json.ts b/src/util/workspace/angular-json.ts index b02e219..5769efc 100644 --- a/src/util/workspace/angular-json.ts +++ b/src/util/workspace/angular-json.ts @@ -8,123 +8,123 @@ import { WorkspaceProject } from 'schematics-utilities'; import { WorkspaceTool } from '@angular-devkit/core/src/experimental/workspace'; export class AngularWorkspace { - tree: Tree; - workspacePath: string; - schema: experimental.workspace.WorkspaceSchema; - content: string; - projectName: string; - project: WorkspaceProject; - target: string; - configuration: string; - path: string; + tree: Tree; + workspacePath: string; + schema: experimental.workspace.WorkspaceSchema; + content: string; + projectName: string; + project: WorkspaceProject; + target: string; + configuration: string; + path: string; - constructor(tree: Tree, options: any) { - this.tree = tree; - this.workspacePath = this.getPath(); - this.content = this.getContent(); - this.schema = this.getWorkspace(); - this.projectName = this.getProjectName(options); - this.project = this.getProject(options); - this.target = 'build'; // TODO allow configuration of other options - this.configuration = 'production'; - this.path = this.project.architect ? this.project.architect[this.target].options.outputPath : `dist/${ this.projectName }`; + constructor(tree: Tree, options: any) { + this.tree = tree; + this.workspacePath = this.getPath(); + this.content = this.getContent(); + this.schema = this.getWorkspace(); + this.projectName = this.getProjectName(options); + this.project = this.getProject(options); + this.target = 'build'; // TODO allow configuration of other options + this.configuration = 'production'; + this.path = this.project.architect + ? this.project.architect[this.target].options.outputPath + : `dist/${this.projectName}`; + } + + getPath() { + const possibleFiles = ['/angular.json', '/.angular.json']; + const path = possibleFiles.filter(file => this.tree.exists(file))[0]; + return path; + } + + getContent() { + const configBuffer = this.tree.read(this.workspacePath); + if (configBuffer === null) { + throw new SchematicsException(`Could not find angular.json`); + } + return configBuffer.toString(); + } + + getWorkspace() { + let schema: experimental.workspace.WorkspaceSchema; + try { + schema = (parseJson(this.content, JsonParseMode.Loose) as {}) as experimental.workspace.WorkspaceSchema; + } catch (e) { + throw new SchematicsException(`Could not parse angular.json: ` + e.message); } - getPath() { - const possibleFiles = ['/angular.json', '/.angular.json']; - const path = possibleFiles.filter(file => this.tree.exists(file))[0]; - return path; + return schema; + } + + getProjectName(options: any) { + let projectName = options.project; + + if (!projectName) { + if (this.schema.defaultProject) { + projectName = this.schema.defaultProject; + } else { + throw new SchematicsException('No project selected and no default project in the workspace'); + } } - getContent() { - const configBuffer = this.tree.read(this.workspacePath); - if (configBuffer === null) { - throw new SchematicsException(`Could not find angular.json`); - } - return configBuffer.toString(); + return projectName; + } + + getProject(options: any) { + const project = this.schema.projects[this.projectName]; + if (!project) { + throw new SchematicsException('Project is not defined in this workspace'); } - getWorkspace() { - let schema: experimental.workspace.WorkspaceSchema; - try { - schema = parseJson( - this.content, - JsonParseMode.Loose - ) as {} as experimental.workspace.WorkspaceSchema; - } catch (e) { - throw new SchematicsException(`Could not parse angular.json: ` + e.message); - } - - return schema; + if (project.projectType !== 'application') { + throw new SchematicsException(`Deploy requires a project type of "application" in angular.json`); } - getProjectName(options: any) { - let projectName = options.project; - - if (!projectName) { - if (this.schema.defaultProject) { - projectName = this.schema.defaultProject; - } else { - throw new SchematicsException('No project selected and no default project in the workspace'); - } - } - - return projectName; + if ( + !project.architect || + !project.architect.build || + !project.architect.build.options || + !project.architect.build.options.outputPath + ) { + throw new SchematicsException( + `Cannot read the output path (architect.build.options.outputPath) of project "${this.projectName}" in angular.json` + ); } - getProject(options: any) { + return project; + } - const project = this.schema.projects[this.projectName]; - if (!project) { - throw new SchematicsException('Project is not defined in this workspace'); - } - - if (project.projectType !== 'application') { - throw new SchematicsException(`Deploy requires a project type of "application" in angular.json`); - } - - if (!project.architect || - !project.architect.build || - !project.architect.build.options || - !project.architect.build.options.outputPath) { - throw new SchematicsException( - `Cannot read the output path (architect.build.options.outputPath) of project "${ this.projectName }" in angular.json`); - } - - return project; + getArchitect(): WorkspaceTool { + if (!this || !this.project || !this.project.architect) { + throw new SchematicsException('An error has occurred while retrieving project configuration.'); } - getArchitect(): WorkspaceTool { - if (!this || !this.project || !this.project.architect) { - throw new SchematicsException('An error has occurred while retrieving project configuration.'); - } + return this.project.architect; + } - return this.project.architect; - } + updateTree() { + this.tree.overwrite(this.workspacePath, JSON.stringify(this.schema, null, 2)); + } - updateTree() { - this.tree.overwrite(this.workspacePath, JSON.stringify(this.schema, null, 2)); - } + addLogoutArchitect() { + this.getArchitect()['azureLogout'] = { + builder: '@azure/ng-deploy:logout' + }; - addLogoutArchitect() { - this.getArchitect()['azureLogout'] = { - builder: '@azure/ng-deploy:logout' - }; + this.updateTree(); + } - this.updateTree(); - } - - addDeployArchitect() { - this.getArchitect()['deploy'] = { - builder: '@azure/ng-deploy:deploy', - options: { - host: 'Azure', - type: 'static', - config: 'azure.json' - } - }; - - this.updateTree(); - } + addDeployArchitect() { + this.getArchitect()['deploy'] = { + builder: '@azure/ng-deploy:deploy', + options: { + host: 'Azure', + type: 'static', + config: 'azure.json' + } + }; + this.updateTree(); + } } diff --git a/src/util/workspace/azure-json.ts b/src/util/workspace/azure-json.ts index 3042a6f..1faf5cc 100644 --- a/src/util/workspace/azure-json.ts +++ b/src/util/workspace/azure-json.ts @@ -7,84 +7,84 @@ import { SchematicsException, Tree } from '@angular-devkit/schematics'; const azureJsonFile = 'azure.json'; export interface AzureDeployConfig { - subscription: string; - resourceGroupName: string; - account: string; + subscription: string; + resourceGroupName: string; + account: string; } export interface AppDeployConfig { - project: string; - target: string; - path: string; - configuration?: string; + project: string; + target: string; + path: string; + configuration?: string; } export interface AzureHostingConfig { - azureHosting: AzureDeployConfig; - app: AppDeployConfig; + azureHosting: AzureDeployConfig; + app: AppDeployConfig; } export interface AzureJSON { - hosting: AzureHostingConfig[]; + hosting: AzureHostingConfig[]; } export function readAzureJson(tree: Tree): AzureJSON | null { - return tree.exists(azureJsonFile) ? safeReadJSON(azureJsonFile, tree) : null; + return tree.exists(azureJsonFile) ? safeReadJSON(azureJsonFile, tree) : null; } export function generateAzureJson(tree: Tree, appDeployConfig: AppDeployConfig, azureDeployConfig: AzureDeployConfig) { - const azureJson: AzureJSON = readAzureJson(tree) || emptyAzureJson(); - const existingHostingConfigIndex = getAzureHostingConfigIndex(azureJson, appDeployConfig.project); - const hostingConfig = generateHostingConfig(appDeployConfig, azureDeployConfig); + const azureJson: AzureJSON = readAzureJson(tree) || emptyAzureJson(); + const existingHostingConfigIndex = getAzureHostingConfigIndex(azureJson, appDeployConfig.project); + const hostingConfig = generateHostingConfig(appDeployConfig, azureDeployConfig); - if (existingHostingConfigIndex >= 0) { - azureJson.hosting[existingHostingConfigIndex] = hostingConfig; - } else { - azureJson.hosting.push(hostingConfig); - } + if (existingHostingConfigIndex >= 0) { + azureJson.hosting[existingHostingConfigIndex] = hostingConfig; + } else { + azureJson.hosting.push(hostingConfig); + } - overwriteIfExists(tree, azureJsonFile, stringifyFormatted(azureJson)); + overwriteIfExists(tree, azureJsonFile, stringifyFormatted(azureJson)); } export function getAzureHostingConfig(azureJson: AzureJSON, projectName: string): AzureHostingConfig | undefined { - return azureJson.hosting.find(config => config.app.project === projectName); + return azureJson.hosting.find(config => config.app.project === projectName); } function getAzureHostingConfigIndex(azureJson: AzureJSON, project: string): number { - return azureJson.hosting.findIndex(config => config.app.project === project); + return azureJson.hosting.findIndex(config => config.app.project === project); } const overwriteIfExists = (tree: Tree, path: string, content: string) => { - if (tree.exists(path)) { - tree.overwrite(path, content); - } else { - tree.create(path, content); - } + if (tree.exists(path)) { + tree.overwrite(path, content); + } else { + tree.create(path, content); + } }; const stringifyFormatted = (obj: any) => JSON.stringify(obj, null, 2); function emptyAzureJson() { - return { - hosting: [] - }; + return { + hosting: [] + }; } function safeReadJSON(path: string, tree: Tree) { - try { - const json = tree.read(path); - if (!json) { - throw new Error(); - } - return JSON.parse(json.toString()); - } catch (e) { - throw new SchematicsException(`Error when parsing ${ path }: ${ e.message }`); + try { + const json = tree.read(path); + if (!json) { + throw new Error(); } + return JSON.parse(json.toString()); + } catch (e) { + throw new SchematicsException(`Error when parsing ${path}: ${e.message}`); + } } function generateHostingConfig(appDeployConfig: AppDeployConfig, azureDeployConfig: AzureDeployConfig) { - return { - app: appDeployConfig, - azureHosting: azureDeployConfig - }; + return { + app: appDeployConfig, + azureHosting: azureDeployConfig + }; } From 6dbe28aad5a23e00adebcbc4341f9523e48a3d4f Mon Sep 17 00:00:00 2001 From: Yohan Lasorsa Date: Thu, 8 Aug 2019 12:18:40 +0200 Subject: [PATCH 07/10] test: uniformize test format --- .../{__tests__/locations.ts => locations.spec.ts} | 2 +- .../resource-group.ts => resource-group.spec.ts} | 12 ++++++------ .../subscription.ts => subscription.spec.ts} | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) rename src/util/azure/{__tests__/locations.ts => locations.spec.ts} (94%) rename src/util/azure/{__tests__/resource-group.ts => resource-group.spec.ts} (86%) rename src/util/azure/{__tests__/subscription.ts => subscription.spec.ts} (97%) diff --git a/src/util/azure/__tests__/locations.ts b/src/util/azure/locations.spec.ts similarity index 94% rename from src/util/azure/__tests__/locations.ts rename to src/util/azure/locations.spec.ts index 2eac794..8b9b827 100644 --- a/src/util/azure/__tests__/locations.ts +++ b/src/util/azure/locations.spec.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getLocation } from '../locations'; +import { getLocation } from './locations'; describe('location', () => { test('should return undefined when locationName is undefined', () => { diff --git a/src/util/azure/__tests__/resource-group.ts b/src/util/azure/resource-group.spec.ts similarity index 86% rename from src/util/azure/__tests__/resource-group.ts rename to src/util/azure/resource-group.spec.ts index f105c30..549b153 100644 --- a/src/util/azure/__tests__/resource-group.ts +++ b/src/util/azure/resource-group.spec.ts @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getResourceGroup, ResourceGroup } from '../resource-group'; +import { getResourceGroup, ResourceGroup } from './resource-group'; import { DeviceTokenCredentials } from '@azure/ms-rest-nodeauth'; -import { AddOptions } from '../../shared/types'; +import { AddOptions } from '../shared/types'; const RESOURCE_GROUP = 'GROUP'; @@ -20,11 +20,11 @@ const logger = { fatal: jest.fn() }; -jest.mock('../resource-group-helper'); -jest.mock('../../prompt/name-generator'); -jest.mock('../../prompt/spinner'); +jest.mock('./resource-group-helper'); +jest.mock('../prompt/name-generator'); +jest.mock('../prompt/spinner'); -import { createResourceGroup } from '../resource-group-helper'; +import { createResourceGroup } from './resource-group-helper'; const createResourceGroupMock: jest.Mock = >createResourceGroup; describe('resource group', () => { diff --git a/src/util/azure/__tests__/subscription.ts b/src/util/azure/subscription.spec.ts similarity index 97% rename from src/util/azure/__tests__/subscription.ts rename to src/util/azure/subscription.spec.ts index 39f56ee..bda1b80 100644 --- a/src/util/azure/__tests__/subscription.ts +++ b/src/util/azure/subscription.spec.ts @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { selectSubscription } from '../subscription'; +import { selectSubscription } from './subscription'; import { LinkedSubscription } from '@azure/ms-rest-nodeauth'; -import { AddOptions } from '../../shared/types'; +import { AddOptions } from '../shared/types'; jest.mock('inquirer'); From ba46cbec089ba1e43b27a72620b7970726903281 Mon Sep 17 00:00:00 2001 From: Yohan Lasorsa Date: Thu, 8 Aug 2019 12:19:19 +0200 Subject: [PATCH 08/10] test: add coverage --- .gitignore | 2 +- .npmignore | 1 + package.json | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ddd18e0..5c7e95f 100644 --- a/.gitignore +++ b/.gitignore @@ -335,7 +335,6 @@ src/**/*.js !src/__mocks__/*.js src/**/*.js.map src/**/*.d.ts -!src/@types/progress.d.ts lib/**/* # IDEs @@ -355,3 +354,4 @@ yarn.lock .DS_Store out/ +coverage/ diff --git a/.npmignore b/.npmignore index afc1c8b..bc976fc 100644 --- a/.npmignore +++ b/.npmignore @@ -2,3 +2,4 @@ *.test.json __mocks__ __tests__ +coverage/ diff --git a/package.json b/package.json index a7d847c..e1df418 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "format:check": "prettier -l \"./src/**/*.{json,ts}\"", "test:jest": "jest", "test:jest:watch": "jest --watch", + "test:coverage": "jest --coverage", "copy:builders:json": "cp ./src/builders/*.json ./out/builders", "copy:ngadd:json": "cp ./src/ng-add/*.json ./out/ng-add" }, From 60d155220c5cddf845be88009728b3ff92f78479 Mon Sep 17 00:00:00 2001 From: Yohan Lasorsa Date: Thu, 8 Aug 2019 12:20:30 +0200 Subject: [PATCH 09/10] test: remove useless manual mock --- src/util/prompt/__mocks__/spinner.ts | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/util/prompt/__mocks__/spinner.ts diff --git a/src/util/prompt/__mocks__/spinner.ts b/src/util/prompt/__mocks__/spinner.ts deleted file mode 100644 index da15d7b..0000000 --- a/src/util/prompt/__mocks__/spinner.ts +++ /dev/null @@ -1,6 +0,0 @@ -console.log('using mock spinner'); -export const spinner = { - start: jest.fn(), - stop: jest.fn(), - succeed: jest.fn() -}; From 1ebac2cd0eb341d39f06eba8625901ae4bfa00ed Mon Sep 17 00:00:00 2001 From: Yohan Lasorsa Date: Thu, 8 Aug 2019 12:22:30 +0200 Subject: [PATCH 10/10] fix: exclude tests from build --- tsconfig.json | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index c53f886..e2319b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "baseUrl": "tsconfig", - "lib": [ - "es2018", - "es2015", - "dom" - ], + "lib": ["es2018", "dom"], "outDir": "out", "emitDecoratorMetadata": true, "experimentalDecorators": true, @@ -24,17 +20,8 @@ "sourceMap": true, "strictNullChecks": true, "target": "es6", - "types": [ - "jest", - "node" - ] + "types": ["jest", "node"] }, - "include": [ - "src/**/*" - ], - "exclude": [ - "src/*/files/**/*", - "**/__mocks__/*", - "**/__tests__/*" - ] + "include": ["src/**/*"], + "exclude": ["**/*.spec.ts", "src/*/files/**/*", "**/__mocks__/*", "**/__tests__/*"] }