Merge pull request #45 from Azure/chore/cleanup
style: add prettier + cleanups
This commit is contained in:
Коммит
3a6bacedff
|
@ -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/
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
*.test.json
|
||||
__mocks__
|
||||
__tests__
|
||||
coverage/
|
||||
|
|
|
@ -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": [
|
||||
"<rootDir>/src"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
};
|
46
package.json
46
package.json
|
@ -11,14 +11,20 @@
|
|||
"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",
|
||||
"test:coverage": "jest --coverage",
|
||||
"copy:builders:json": "cp ./src/builders/*.json ./out/builders",
|
||||
"copy:ngadd:json": "cp ./src/ng-add/*.json ./out/ng-add"
|
||||
},
|
||||
"keywords": [
|
||||
"schematics"
|
||||
"schematics",
|
||||
"angular",
|
||||
"azure",
|
||||
"deploy"
|
||||
],
|
||||
"author": {
|
||||
"name": "Shmuela Jacobs",
|
||||
|
@ -32,6 +38,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/",
|
||||
|
@ -59,11 +69,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 +78,40 @@
|
|||
"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",
|
||||
"@types/progress": "^2.0.3",
|
||||
"husky": "^3.0.2",
|
||||
"jest": "^24.8.0",
|
||||
"prettier": "^1.16.4",
|
||||
"prettier": "^1.18.2",
|
||||
"pretty-quick": "^1.11.1",
|
||||
"schematics-utilities": "^1.1.2",
|
||||
"ts-jest": "^24.0.2",
|
||||
"tslint-angular": "^1.1.2",
|
||||
"typescript": "~3.4.0"
|
||||
},
|
||||
"jest": {
|
||||
"roots": [
|
||||
"<rootDir>/src"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "pretty-quick --staged"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
declare module 'progress';
|
|
@ -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<void> {
|
||||
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');
|
||||
}
|
||||
|
|
|
@ -11,33 +11,37 @@ import { AzureHostingConfig, AzureJSON } from '../util/workspace/azure-json';
|
|||
import deploy from './actions/deploy';
|
||||
|
||||
export default createBuilder<any>(
|
||||
async (builderConfig: any, context: BuilderContext): Promise<BuilderOutput> => {
|
||||
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<BuilderOutput> => {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/ar
|
|||
import { clearCreds } from '../util/azure/auth';
|
||||
|
||||
export default createBuilder<any>(
|
||||
async (builderConfig: any, context: BuilderContext): Promise<BuilderOutput> => {
|
||||
await clearCreds();
|
||||
context.logger.info('Cleared Azure credentials from cache.');
|
||||
return { success: true };
|
||||
}
|
||||
async (builderConfig: any, context: BuilderContext): Promise<BuilderOutput> => {
|
||||
await clearCreds();
|
||||
context.logger.info('Cleared Azure credentials from cache.');
|
||||
return { success: true };
|
||||
}
|
||||
);
|
||||
|
|
|
@ -28,8 +28,12 @@ describe('ng add @azure/ng-deploy', () => {
|
|||
const testRunner = new SchematicTestRunner('schematics', collectionPath);
|
||||
|
||||
async function initAngularProject(): Promise<UnitTestTree> {
|
||||
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'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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: []
|
||||
});
|
||||
|
|
|
@ -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 }));
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
||||
export const selectSubscription = () => Promise.resolve('fake-subscription-1234');
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<string | AuthResponse | null>({
|
||||
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<AuthResponse> {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
@ -15,4 +15,4 @@ describe('location', () => {
|
|||
expect(actual && actual.id).toBe('southafricanorth');
|
||||
expect(actual && actual.name).toBe('South Africa North');
|
||||
});
|
||||
})
|
||||
});
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<ResourceGroupsCreateOrUpdateResponse> {
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,10 +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';
|
||||
|
||||
|
@ -21,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<any, any> = <jest.Mock<any, any>>createResourceGroup;
|
||||
|
||||
describe('resource group', () => {
|
||||
|
@ -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);
|
|
@ -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<ResourceGroup> {
|
||||
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<StorageLocation> {
|
||||
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));
|
||||
}
|
||||
|
|
|
@ -2,26 +2,25 @@
|
|||
* 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');
|
||||
|
||||
// AddOptions, Logger
|
||||
|
||||
// AddOptions, Logger
|
||||
|
||||
const SUBID = '124';
|
||||
const SUBNAME = 'name';
|
||||
|
||||
const optionsMock = <AddOptions>{
|
||||
const optionsMock = <AddOptions>{
|
||||
subscriptionId: SUBID,
|
||||
subscriptionName: SUBNAME
|
||||
};
|
||||
|
||||
// const optionsMockEmpty = <AddOptions>{};
|
||||
|
||||
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 = <Array<LinkedSubscription>>[{
|
||||
id: '456',
|
||||
name: 'a sub'
|
||||
}];
|
||||
|
||||
test('provided sub id DOES NOT match when provided in options', async () => {
|
||||
const subs = <Array<LinkedSubscription>>[
|
||||
{
|
||||
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 = <Array<LinkedSubscription>>[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 = <Array<LinkedSubscription>>[
|
||||
{ id: 'abc', name: 'subMock' },
|
||||
{ id: '123', name: 'sub2' }
|
||||
];
|
||||
const subs = <Array<LinkedSubscription>>[{ 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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<string> {
|
||||
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."
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
console.log('using mock spinner');
|
||||
export const spinner = {
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
succeed: jest.fn()
|
||||
}
|
||||
|
|
@ -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<string>);
|
||||
title?: string;
|
||||
validate?: any;
|
||||
id: string;
|
||||
name?: string;
|
||||
message: string;
|
||||
default?: string;
|
||||
defaultGenerator?: (name: string) => Promise<string>;
|
||||
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 };
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<boolean>) {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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__/*"]
|
||||
}
|
||||
|
|
69
tslint.json
69
tslint.json
|
@ -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"]
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче