From 8d1c0bbdad8ba522846f90487293221636ce4a61 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Thu, 11 Oct 2018 12:28:58 +0100 Subject: [PATCH] fix(@angular/cli): error out when command json is invalid --- packages/angular/cli/commands/e2e-long.md | 2 + packages/angular/cli/commands/e2e.json | 2 +- .../angular/cli/commands/generate-impl.ts | 1 - packages/angular/cli/models/command-runner.ts | 2 +- packages/angular/cli/utilities/json-schema.ts | 17 +++-- .../angular/cli/utilities/json-schema_spec.ts | 76 +++++++++++++++++++ scripts/snapshots.ts | 6 +- .../e2e/tests/commands/help/help-json.ts | 8 ++ 8 files changed, 102 insertions(+), 12 deletions(-) create mode 100644 packages/angular/cli/commands/e2e-long.md create mode 100644 packages/angular/cli/utilities/json-schema_spec.ts create mode 100644 tests/legacy-cli/e2e/tests/commands/help/help-json.ts diff --git a/packages/angular/cli/commands/e2e-long.md b/packages/angular/cli/commands/e2e-long.md new file mode 100644 index 000000000000..028becbcde67 --- /dev/null +++ b/packages/angular/cli/commands/e2e-long.md @@ -0,0 +1,2 @@ +Must be executed from within a workspace directory. +When a project name is not supplied, the configured default e2e project of the workspace is used. \ No newline at end of file diff --git a/packages/angular/cli/commands/e2e.json b/packages/angular/cli/commands/e2e.json index 5dd23c01dfe7..073fdff3a684 100644 --- a/packages/angular/cli/commands/e2e.json +++ b/packages/angular/cli/commands/e2e.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/schema", "$id": "ng-cli://commands/e2e.json", "description": "Builds and serves an Angular app, and runs end-to-end tests using Protractor.", - "$longDescription": "Must be executed from within a workspace directory.\n When a project name is not supplied, the configured default e2e project of the workspace is used.", + "$longDescription": "./e2e-long.md", "$aliases": [ "e" ], "$scope": "in", diff --git a/packages/angular/cli/commands/generate-impl.ts b/packages/angular/cli/commands/generate-impl.ts index 49d9f60056c4..b47808a7e1f9 100644 --- a/packages/angular/cli/commands/generate-impl.ts +++ b/packages/angular/cli/commands/generate-impl.ts @@ -36,7 +36,6 @@ export class GenerateCommand extends SchematicCommand { schematic.description.path, this._workflow.registry, schematic.description.schemaJson, - this.logger, ); } else { continue; diff --git a/packages/angular/cli/models/command-runner.ts b/packages/angular/cli/models/command-runner.ts index a44c4f1a0755..e4d304b58f1e 100644 --- a/packages/angular/cli/models/command-runner.ts +++ b/packages/angular/cli/models/command-runner.ts @@ -93,7 +93,7 @@ export async function runCommand( } commandMap[name] = - await parseJsonSchemaToCommandDescription(name, schemaPath, registry, schema, logger); + await parseJsonSchemaToCommandDescription(name, schemaPath, registry, schema); } let commandName: string | undefined = undefined; diff --git a/packages/angular/cli/utilities/json-schema.ts b/packages/angular/cli/utilities/json-schema.ts index 40714c90651e..a857867490c6 100644 --- a/packages/angular/cli/utilities/json-schema.ts +++ b/packages/angular/cli/utilities/json-schema.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import { json, logging } from '@angular-devkit/core'; +import { BaseException, json } from '@angular-devkit/core'; import { ExportStringRef } from '@angular-devkit/schematics/tools'; import { readFileSync } from 'fs'; import { dirname, resolve } from 'path'; @@ -19,6 +19,13 @@ import { Value, } from '../models/interface'; + +export class CommandJsonPathException extends BaseException { + constructor(public readonly path: string, public readonly name: string) { + super(`File ${path} was not found while constructing the subcommand ${name}.`); + } +} + function _getEnumFromValue( value: json.JsonValue, enumeration: E, @@ -40,7 +47,6 @@ export async function parseJsonSchemaToSubCommandDescription( jsonPath: string, registry: json.schema.SchemaRegistry, schema: json.JsonObject, - logger: logging.Logger, ): Promise { const options = await parseJsonSchemaToOptions(registry, schema); @@ -69,7 +75,7 @@ export async function parseJsonSchemaToSubCommandDescription( try { longDescription = readFileSync(ldPath, 'utf-8'); } catch (e) { - logger.warn(`File ${ldPath} was not found while constructing the subcommand ${name}.`); + throw new CommandJsonPathException(ldPath, name); } } let usageNotes = ''; @@ -78,7 +84,7 @@ export async function parseJsonSchemaToSubCommandDescription( try { usageNotes = readFileSync(unPath, 'utf-8'); } catch (e) { - logger.warn(`File ${unPath} was not found while constructing the subcommand ${name}.`); + throw new CommandJsonPathException(unPath, name); } } @@ -99,10 +105,9 @@ export async function parseJsonSchemaToCommandDescription( jsonPath: string, registry: json.schema.SchemaRegistry, schema: json.JsonObject, - logger: logging.Logger, ): Promise { const subcommand = - await parseJsonSchemaToSubCommandDescription(name, jsonPath, registry, schema, logger); + await parseJsonSchemaToSubCommandDescription(name, jsonPath, registry, schema); // Before doing any work, let's validate the implementation. if (typeof schema.$impl != 'string') { diff --git a/packages/angular/cli/utilities/json-schema_spec.ts b/packages/angular/cli/utilities/json-schema_spec.ts new file mode 100644 index 000000000000..c26601227c3b --- /dev/null +++ b/packages/angular/cli/utilities/json-schema_spec.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + * + */ +import { schema } from '@angular-devkit/core'; +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { of } from 'rxjs'; +import { CommandJsonPathException, parseJsonSchemaToCommandDescription } from './json-schema'; + +describe('parseJsonSchemaToCommandDescription', () => { + let registry: schema.CoreSchemaRegistry; + const baseSchemaJson = { + '$schema': 'http://json-schema.org/schema', + '$id': 'ng-cli://commands/version.json', + 'description': 'Outputs Angular CLI version.', + '$longDescription': 'not a file ref', + + '$aliases': ['v'], + '$scope': 'all', + '$impl': './version-impl#VersionCommand', + + 'type': 'object', + 'allOf': [ + { '$ref': './definitions.json#/definitions/base' }, + ], + }; + + beforeEach(() => { + registry = new schema.CoreSchemaRegistry([]); + registry.registerUriHandler((uri: string) => { + if (uri.startsWith('ng-cli://')) { + const content = readFileSync( + join(__dirname, '..', uri.substr('ng-cli://'.length)), 'utf-8'); + + return of(JSON.parse(content)); + } else { + return null; + } + }); + }); + + it(`should throw on invalid $longDescription path`, async () => { + const name = 'version'; + const schemaPath = join(__dirname, './bad-sample.json'); + const schemaJson = { ...baseSchemaJson, $longDescription: 'not a file ref' }; + try { + await parseJsonSchemaToCommandDescription(name, schemaPath, registry, schemaJson); + } catch (error) { + const refPath = join(__dirname, schemaJson.$longDescription); + expect(error).toEqual(new CommandJsonPathException(refPath, name)); + + return; + } + expect(true).toBe(false, 'function should have thrown'); + }); + + it(`should throw on invalid $usageNotes path`, async () => { + const name = 'version'; + const schemaPath = join(__dirname, './bad-sample.json'); + const schemaJson = { ...baseSchemaJson, $usageNotes: 'not a file ref' }; + try { + await parseJsonSchemaToCommandDescription(name, schemaPath, registry, schemaJson); + } catch (error) { + const refPath = join(__dirname, schemaJson.$usageNotes); + expect(error).toEqual(new CommandJsonPathException(refPath, name)); + + return; + } + expect(true).toBe(false, 'function should have thrown'); + }); +}); diff --git a/scripts/snapshots.ts b/scripts/snapshots.ts index 1b62b00a1632..a848845066fa 100644 --- a/scripts/snapshots.ts +++ b/scripts/snapshots.ts @@ -117,9 +117,9 @@ export default async function(opts: SnapshotsOptions, logger: logging.Logger) { const options = { cwd: newProjectRoot }; const childLogger = logger.createChild(commandName); const stdout = _exec(ngPath, [commandName, '--help=json'], options, childLogger); - if (stdout.trim()) { - fs.writeFileSync(path.join(helpOutputRoot, commandName + '.json'), stdout); - } + // Make sure the output is JSON before printing it, and format it as well. + const jsonOutput = JSON.stringify(JSON.parse(stdout.trim()), undefined, 2); + fs.writeFileSync(path.join(helpOutputRoot, commandName + '.json'), jsonOutput); } if (!githubToken) { diff --git a/tests/legacy-cli/e2e/tests/commands/help/help-json.ts b/tests/legacy-cli/e2e/tests/commands/help/help-json.ts new file mode 100644 index 000000000000..76768f45311f --- /dev/null +++ b/tests/legacy-cli/e2e/tests/commands/help/help-json.ts @@ -0,0 +1,8 @@ +import { ng } from '../../../utils/process'; + + +export default function () { + return ng('build', '--help=json') + // Output should be valid JSON. + .then(({ stdout }) => JSON.parse(stdout.trim())); +}