Merge branch 'master' into test/e2e

This commit is contained in:
Yohan Lasorsa 2019-08-16 10:33:18 +02:00 коммит произвёл GitHub
Родитель 18ae776d13 063db8c334
Коммит 3077283e3e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
33 изменённых файлов: 970 добавлений и 1074 удалений

2
.gitignore поставляемый
Просмотреть файл

@ -335,7 +335,6 @@ src/**/*.js
!src/__mocks__/*.js
src/**/*.js.map
src/**/*.d.ts
!src/@types/progress.d.ts
lib/**/*
# IDEs
@ -356,3 +355,4 @@ yarn.lock
out/
.e2e-tests/
coverage/

Просмотреть файл

@ -2,3 +2,4 @@
*.test.json
__mocks__
__tests__
coverage/

Просмотреть файл

@ -58,10 +58,10 @@ Once you have completed the previous steps to npm link the local copy of ng-depl
1. You may be prompted you to sign in to Azure, providing a link to open in your browser and a code to paste in the login page.
1. Then, instead of running `ng add @azure/ng-deploy`, add the local version.
1. Then, running `ng add @azure/ng-deploy` will use the locally linked version.
```sh
ng add ng-deploy-azure
ng add @azure/ng-deploy
```
1. Now you can deploy your angular app to azure.

Просмотреть файл

@ -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"
},
};

Просмотреть файл

@ -11,15 +11,21 @@
"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:e2e": "./scripts/test.sh",
"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",
@ -33,6 +39,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/",
@ -60,11 +70,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",
@ -72,27 +79,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
src/@types/progress.d.ts поставляемый
Просмотреть файл

@ -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__/*"]
}

Просмотреть файл

@ -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"]
}
}