From 472f3801d971e5f41be692314a43661901741bf1 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Sun, 10 Dec 2017 16:03:25 -0800 Subject: [PATCH 01/10] test: add an istanbul bootstrap script When the test script is loaded it is already too late, and we are missing a lot of information for coverage. This makes sure we get coverage for everything (except bootstrap itself). --- lib/bootstrap-local.js | 12 ++++--- lib/istanbul-local.js | 57 +++++++++++++++++++++++++++++++++ scripts/test.ts | 72 +++++------------------------------------- 3 files changed, 73 insertions(+), 68 deletions(-) create mode 100644 lib/istanbul-local.js diff --git a/lib/bootstrap-local.js b/lib/bootstrap-local.js index 237e2a49c4..fefb8cffc3 100644 --- a/lib/bootstrap-local.js +++ b/lib/bootstrap-local.js @@ -13,6 +13,12 @@ const path = require('path'); const ts = require('typescript'); +let _istanbulRequireHook = null; +if (process.env['CODE_COVERAGE'] || process.argv.indexOf('--code-coverage') !== -1) { + _istanbulRequireHook = require('./istanbul-local').istanbulRequireHook; +} + + // Check if we need to profile this CLI run. let profiler = null; if (process.env['DEVKIT_PROFILING']) { @@ -45,8 +51,6 @@ Error.stackTraceLimit = Infinity; global._DevKitIsLocal = true; global._DevKitRoot = path.resolve(__dirname, '..'); -global._DevKitRequireHook = null; - const compilerOptions = ts.readConfigFile(path.join(__dirname, '../tsconfig.json'), p => { return fs.readFileSync(p, 'utf-8'); @@ -71,8 +75,8 @@ require.extensions['.ts'] = function (m, filename) { try { let result = ts.transpile(source, compilerOptions['compilerOptions'], filename); - if (global._DevKitRequireHook) { - result = global._DevKitRequireHook(result, filename); + if (_istanbulRequireHook) { + result = _istanbulRequireHook(result, filename); } // Send it to node to execute. diff --git a/lib/istanbul-local.js b/lib/istanbul-local.js new file mode 100644 index 0000000000..ce062344de --- /dev/null +++ b/lib/istanbul-local.js @@ -0,0 +1,57 @@ +/** + * @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 + */ +const { SourceMapConsumer } = require('source-map'); +const Istanbul = require('istanbul'); + +const inlineSourceMapRe = /\/\/# sourceMappingURL=data:application\/json;base64,(\S+)$/; + + +// Use the internal DevKit Hook of the require extension installed by our bootstrapping code to add +// Istanbul (not Constantinople) collection to the code. +const codeMap = new Map(); +exports.codeMap = codeMap; + +exports.istanbulRequireHook = function(code, filename) { + // Skip spec files. + if (filename.match(/_spec\.ts$/)) { + return code; + } + const codeFile = codeMap.get(filename); + if (codeFile) { + return codeFile.code; + } + + const instrumenter = new Istanbul.Instrumenter({ + esModules: true, + codeGenerationOptions: { + sourceMap: filename, + sourceMapWithCode: true, + }, + }); + let instrumentedCode = instrumenter.instrumentSync(code, filename); + const match = code.match(inlineSourceMapRe); + + if (match) { + const sourceMapGenerator = instrumenter.sourceMap; + // Fix source maps for exception reporting (since the exceptions happen in the instrumented + // code. + const sourceMapJson = JSON.parse(Buffer.from(match[1], 'base64').toString()); + const consumer = new SourceMapConsumer(sourceMapJson); + sourceMapGenerator.applySourceMap(consumer, filename); + + instrumentedCode = instrumentedCode.replace(inlineSourceMapRe, '') + + '//# sourceMappingURL=data:application/json;base64,' + + new Buffer(sourceMapGenerator.toString()).toString('base64'); + + // Keep the consumer from the original source map, because the reports from Istanbul (not + // Constantinople) are already mapped against the code. + codeMap.set(filename, { code: instrumentedCode, map: consumer }); + } + + return instrumentedCode; +}; diff --git a/scripts/test.ts b/scripts/test.ts index 64ca18c77f..cb14d2b54e 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -12,11 +12,11 @@ import 'jasmine'; import { SpecReporter as JasmineSpecReporter } from 'jasmine-spec-reporter'; import { ParsedArgs } from 'minimist'; import { join, relative } from 'path'; -import { Position, SourceMapConsumer, SourceMapGenerator } from 'source-map'; +import { Position, SourceMapConsumer } from 'source-map'; import * as ts from 'typescript'; import { packages } from '../lib/packages'; - +const codeMap = require('../lib/istanbul-local').codeMap; const Jasmine = require('jasmine'); const projectBaseDir = join(__dirname, '..'); @@ -25,70 +25,15 @@ require('source-map-support').install({ }); -declare const global: { - _DevKitRequireHook: Function, - __coverage__: CoverageType; -}; - - -interface Instrumenter extends Istanbul.Instrumenter { - sourceMap: SourceMapGenerator; -} - - interface CoverageLocation { start: Position; end: Position; } -type CoverageType = any; // tslint:disable-line:no-any - - -const inlineSourceMapRe = /\/\/# sourceMappingURL=data:application\/json;base64,(\S+)$/; - - -// Use the internal DevKit Hook of the require extension installed by our bootstrapping code to add -// Istanbul (not Constantinople) collection to the code. -const codeMap = new Map(); -function istanbulDevKitRequireHook(code: string, filename: string) { - // Skip spec files. - if (filename.match(/_spec\.ts$/)) { - return code; - } - const codeFile = codeMap.get(filename); - if (codeFile) { - return codeFile.code; - } - - const instrumenter = new Istanbul.Instrumenter({ - esModules: true, - codeGenerationOptions: { - sourceMap: filename, - sourceMapWithCode: true, - }, - }) as Instrumenter; - let instrumentedCode = instrumenter.instrumentSync(code, filename); - const match = code.match(inlineSourceMapRe); - - if (match) { - const sourceMapGenerator: SourceMapGenerator = instrumenter.sourceMap; - // Fix source maps for exception reporting (since the exceptions happen in the instrumented - // code. - const sourceMapJson = JSON.parse(Buffer.from(match[1], 'base64').toString()); - const consumer = new SourceMapConsumer(sourceMapJson); - sourceMapGenerator.applySourceMap(consumer, filename); - - instrumentedCode = instrumentedCode.replace(inlineSourceMapRe, '') - + '//# sourceMappingURL=data:application/json;base64,' - + new Buffer(sourceMapGenerator.toString()).toString('base64'); - - // Keep the consumer from the original source map, because the reports from Istanbul (not - // Constantinople) are already mapped against the code. - codeMap.set(filename, { code: instrumentedCode, map: consumer }); - } - - return instrumentedCode; -} +type CoverageType = any; // tslint:disable-line:no-any +declare const global: { + __coverage__: CoverageType; +}; // Add the Istanbul (not Constantinople) reporter. @@ -158,9 +103,9 @@ class IstanbulReporter implements jasmine.CustomReporter { if (global.__coverage__) { this._updateCoverageJsonSourceMap(global.__coverage__); istanbulCollector.add(global.__coverage__); - } - istanbulReporter.write(istanbulCollector, true, () => {}); + istanbulReporter.write(istanbulCollector, true, () => {}); + } } } @@ -212,7 +157,6 @@ export default function (args: ParsedArgs, logger: logging.Logger) { } if (args['code-coverage']) { - global._DevKitRequireHook = istanbulDevKitRequireHook; runner.env.addReporter(new IstanbulReporter()); } From 4200054e25494c3836e1cca0e3894f210fe39710 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 16 Nov 2017 14:57:37 -0800 Subject: [PATCH 02/10] feat(@angular-devkit/schematics): change schema validation to AJV --- package-lock.json | 32 +- package.json | 1 + .../angular_devkit/schematics/package.json | 3 +- .../schematics/src/engine/engine.ts | 11 +- .../schematics/src/engine/interface.ts | 4 +- .../schematics/src/engine/schematic.ts | 15 +- .../schematics/src/engine/schematic_spec.ts | 2 +- .../angular_devkit/schematics/src/index.ts | 2 +- .../schematics/src/rules/call.ts | 49 +-- .../schematics/src/tree/interface.ts | 7 + .../testing/schematic-test-runner.ts | 24 +- .../schematics/tools/ajv-option-transform.ts | 278 ++++++++++++++++++ .../tools/ajv-option-transform_spec.ts | 76 +++++ .../schematics/tools/fallback-engine-host.ts | 15 +- .../tools/file-system-engine-host-base.ts | 12 +- .../angular_devkit/schematics/tools/index.ts | 3 + .../tools/schema-option-transform.ts | 84 ++++++ .../schematics_cli/bin/schematics.ts | 18 +- 18 files changed, 542 insertions(+), 94 deletions(-) create mode 100644 packages/angular_devkit/schematics/tools/ajv-option-transform.ts create mode 100644 packages/angular_devkit/schematics/tools/ajv-option-transform_spec.ts create mode 100644 packages/angular_devkit/schematics/tools/schema-option-transform.ts diff --git a/package-lock.json b/package-lock.json index 092434d58a..abebd35541 100644 --- a/package-lock.json +++ b/package-lock.json @@ -106,12 +106,14 @@ "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=" }, "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.1.tgz", + "integrity": "sha1-s4u4h22ehr7plJVqBOch6IskjrI=", "requires": { "co": "4.6.0", - "json-stable-stringify": "1.0.1" + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" } }, "align-text": { @@ -932,6 +934,16 @@ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -1972,7 +1984,6 @@ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", "requires": { - "ajv": "4.11.8", "har-schema": "1.0.5" } }, @@ -2320,13 +2331,10 @@ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "requires": { - "jsonify": "0.0.0" - } + "json-schema-traverse": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" }, "json-stringify-safe": { "version": "5.0.1", diff --git a/package.json b/package.json index 0288ae7270..cd8b3c1cac 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@types/source-map": "^0.5.0", "@types/webpack": "^3.0.2", "@types/webpack-sources": "^0.1.3", + "ajv": "^5.5.1", "chokidar": "^1.7.0", "conventional-changelog": "^1.1.0", "glob": "^7.0.3", diff --git a/packages/angular_devkit/schematics/package.json b/packages/angular_devkit/schematics/package.json index a2ffb6ee96..542eb45824 100644 --- a/packages/angular_devkit/schematics/package.json +++ b/packages/angular_devkit/schematics/package.json @@ -17,7 +17,8 @@ ], "dependencies": { "@angular-devkit/core": "0.0.0", - "@ngtools/json-schema": "^1.1.0" + "@ngtools/json-schema": "^1.1.0", + "ajv": "~5.5.1" }, "peerDependencies": { "rxjs": "^5.5.2" diff --git a/packages/angular_devkit/schematics/src/engine/engine.ts b/packages/angular_devkit/schematics/src/engine/engine.ts index 9e40b80a5a..12f8e1d7a6 100644 --- a/packages/angular_devkit/schematics/src/engine/engine.ts +++ b/packages/angular_devkit/schematics/src/engine/engine.ts @@ -7,7 +7,7 @@ */ import { BaseException, logging } from '@angular-devkit/core'; import { CollectionDescription, TypedSchematicContext } from '@angular-devkit/schematics'; -import 'rxjs/add/operator/map'; +import { Observable } from 'rxjs/Observable'; import { Url } from 'url'; import { MergeStrategy } from '../tree/interface'; import { NullTree } from '../tree/null'; @@ -124,11 +124,10 @@ export class SchematicEngine( - schematic: Schematic, options: OptionT): ResultT { - return this._host.transformOptions( - schematic.description, - options, - ); + schematic: Schematic, + options: OptionT, + ): Observable { + return this._host.transformOptions(schematic.description, options); } createSourceFromUrl(url: Url, context: TypedSchematicContext): Source { diff --git a/packages/angular_devkit/schematics/src/engine/interface.ts b/packages/angular_devkit/schematics/src/engine/interface.ts index c1293ab1e4..3b340f75b8 100644 --- a/packages/angular_devkit/schematics/src/engine/interface.ts +++ b/packages/angular_devkit/schematics/src/engine/interface.ts @@ -55,7 +55,7 @@ export interface EngineHost( schematic: SchematicDescription, options: OptionT, - ): ResultT; + ): Observable; readonly defaultMergeStrategy?: MergeStrategy; } @@ -88,7 +88,7 @@ export interface Engine( schematic: Schematic, options: OptionT, - ): ResultT; + ): Observable; readonly defaultMergeStrategy: MergeStrategy; } diff --git a/packages/angular_devkit/schematics/src/engine/schematic.ts b/packages/angular_devkit/schematics/src/engine/schematic.ts index a0ca25e322..5f1d8d4465 100644 --- a/packages/angular_devkit/schematics/src/engine/schematic.ts +++ b/packages/angular_devkit/schematics/src/engine/schematic.ts @@ -9,6 +9,9 @@ import { BaseException } from '@angular-devkit/core'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; import 'rxjs/add/operator/concatMap'; +import { concatMap } from 'rxjs/operators/concatMap'; +import { first } from 'rxjs/operators/first'; +import { map } from 'rxjs/operators/map'; import { callRule } from '../rules/call'; import { Tree } from '../tree/interface'; import { @@ -49,8 +52,16 @@ export class SchematicImpl>, ): Observable { const context = this._engine.createContext(this, parentContext); - const transformedOptions = this._engine.transformOptions(this, options); - return callRule(this._factory(transformedOptions), host, context); + return host + .pipe( + first(), + concatMap(tree => this._engine.transformOptions(this, options).pipe( + map(o => [tree, o]), + )), + concatMap(([tree, transformedOptions]: [Tree, OptionT]) => { + return callRule(this._factory(transformedOptions), Observable.of(tree), context); + }), + ); } } diff --git a/packages/angular_devkit/schematics/src/engine/schematic_spec.ts b/packages/angular_devkit/schematics/src/engine/schematic_spec.ts index b6f77c9103..85798203ea 100644 --- a/packages/angular_devkit/schematics/src/engine/schematic_spec.ts +++ b/packages/angular_devkit/schematics/src/engine/schematic_spec.ts @@ -33,7 +33,7 @@ const context = { }; const engine: Engine = { createContext: (schematic: Schematic<{}, {}>) => ({ engine, schematic, ...context }), - transformOptions: (_: {}, options: {}) => options, + transformOptions: (_: {}, options: {}) => Observable.of(options), defaultMergeStrategy: MergeStrategy.Default, } as {} as Engine; const collection = { diff --git a/packages/angular_devkit/schematics/src/index.ts b/packages/angular_devkit/schematics/src/index.ts index dd584f391d..60c9cc706f 100644 --- a/packages/angular_devkit/schematics/src/index.ts +++ b/packages/angular_devkit/schematics/src/index.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import { FilePredicate, MergeStrategy } from './tree/interface'; -import {Tree as TreeInterface } from './tree/interface'; +import { Tree as TreeInterface } from './tree/interface'; import { branch, empty, merge, optimize, partition } from './tree/static'; diff --git a/packages/angular_devkit/schematics/src/rules/call.ts b/packages/angular_devkit/schematics/src/rules/call.ts index cf3a0009cb..a0d703d0d7 100644 --- a/packages/angular_devkit/schematics/src/rules/call.ts +++ b/packages/angular_devkit/schematics/src/rules/call.ts @@ -9,6 +9,7 @@ import { BaseException } from '@angular-devkit/core'; import { Observable } from 'rxjs/Observable'; import { _throw } from 'rxjs/observable/throw'; import { last } from 'rxjs/operators/last'; +import { mergeMap } from 'rxjs/operators/mergeMap'; import { tap } from 'rxjs/operators/tap'; import { Rule, SchematicContext, Source } from '../engine/interface'; import { Tree, TreeSymbol } from '../tree/interface'; @@ -83,29 +84,31 @@ export function callSource(source: Source, context: SchematicContext): Observabl export function callRule(rule: Rule, input: Observable, context: SchematicContext): Observable { - return input.mergeMap(inputTree => { - const result = rule(inputTree, context) as object; + return input.pipe( + mergeMap(inputTree => { + const result = rule(inputTree, context) as object; - if (result === undefined) { - return Observable.of(inputTree); - } else if (TreeSymbol in result) { - return Observable.of(result as Tree); - } else if (Symbol.observable in result) { - const obs = result as Observable; + if (result === undefined) { + return Observable.of(inputTree); + } else if (TreeSymbol in result) { + return Observable.of(result as Tree); + } else if (Symbol.observable in result) { + const obs = result as Observable; - // Only return the last Tree, and make sure it's a Tree. - return obs.pipe( - last(), - tap(inner => { - if (!(TreeSymbol in inner)) { - throw new InvalidRuleResultException(inner); - } - }), - ); - } else if (result === undefined) { - return Observable.of(inputTree); - } else { - return _throw(new InvalidRuleResultException(result)); - } - }); + // Only return the last Tree, and make sure it's a Tree. + return obs.pipe( + last(), + tap(inner => { + if (!(TreeSymbol in inner)) { + throw new InvalidRuleResultException(inner); + } + }), + ); + } else if (result === undefined) { + return Observable.of(inputTree); + } else { + return _throw(new InvalidRuleResultException(result)); + } + }), + ); } diff --git a/packages/angular_devkit/schematics/src/tree/interface.ts b/packages/angular_devkit/schematics/src/tree/interface.ts index ee20d0a546..a43d5f0005 100644 --- a/packages/angular_devkit/schematics/src/tree/interface.ts +++ b/packages/angular_devkit/schematics/src/tree/interface.ts @@ -105,6 +105,13 @@ export interface Tree { } +namespace Tree { + export function isTree(maybeTree: object): maybeTree is Tree { + return TreeSymbol in maybeTree; + } +} + + export interface UpdateRecorder { // These just record changes. insertLeft(index: number, content: Buffer | string): UpdateRecorder; diff --git a/packages/angular_devkit/schematics/testing/schematic-test-runner.ts b/packages/angular_devkit/schematics/testing/schematic-test-runner.ts index 332d115a8e..c8a059b7f8 100644 --- a/packages/angular_devkit/schematics/testing/schematic-test-runner.ts +++ b/packages/angular_devkit/schematics/testing/schematic-test-runner.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 { logging, schema } from '@angular-devkit/core'; +import { logging } from '@angular-devkit/core'; import { Collection, DelegateTree, @@ -17,8 +17,9 @@ import { VirtualTree, } from '@angular-devkit/schematics'; import { - FileSystemSchematicDesc, + AjvSchemaRegistry, NodeModulesTestEngineHost, + validateOptionsWithSchema, } from '@angular-devkit/schematics/tools'; import { Observable } from 'rxjs/Observable'; import { callRule } from '../src/rules/call'; @@ -38,29 +39,12 @@ export class SchematicTestRunner { private _engine: SchematicEngine<{}, {}> = new SchematicEngine(this._engineHost); private _collection: Collection<{}, {}>; private _logger: logging.Logger; - private _registry: schema.JsonSchemaRegistry; constructor(private _collectionName: string, collectionPath: string) { this._engineHost.registerCollection(_collectionName, collectionPath); this._logger = new logging.Logger('test'); - this._registry = new schema.JsonSchemaRegistry(); - - this._engineHost.registerOptionsTransform((schematicDescription: {}, opts: object) => { - const schematic: FileSystemSchematicDesc = schematicDescription as FileSystemSchematicDesc; - - if (schematic.schema && schematic.schemaJson) { - const schemaJson = schematic.schemaJson as schema.JsonSchemaObject; - const name = schemaJson.id || schematic.name; - this._registry.addSchema(name, schemaJson); - const serializer = new schema.serializers.JavascriptSerializer(); - const fn = serializer.serialize(name, this._registry); - - return fn(opts); - } - - return opts; - }); + this._engineHost.registerOptionsTransform(validateOptionsWithSchema(new AjvSchemaRegistry())); this._collection = this._engine.createCollection(this._collectionName); } diff --git a/packages/angular_devkit/schematics/tools/ajv-option-transform.ts b/packages/angular_devkit/schematics/tools/ajv-option-transform.ts new file mode 100644 index 0000000000..398fdc741b --- /dev/null +++ b/packages/angular_devkit/schematics/tools/ajv-option-transform.ts @@ -0,0 +1,278 @@ +/** + * @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 { JsonArray, JsonObject } from '@angular-devkit/core'; +import * as ajv from 'ajv'; +import * as http from 'http'; +import { Observable } from 'rxjs/Observable'; +import { fromPromise } from 'rxjs/observable/fromPromise'; +import { map } from 'rxjs/operators/map'; +import { + OptionsSchemaRegistry, + OptionsSchemaValidator, + OptionsSchemaValidatorResult, +} from './schema-option-transform'; + + +function _parseJsonPointer(pointer: string): string[] { + if (pointer === '') { return []; } + if (pointer.charAt(0) !== '/') { throw new Error('Invalid JSON pointer: ' + pointer); } + + return pointer.substring(1).split(/\//).map(str => str.replace(/~1/g, '/').replace(/~0/g, '~')); +} + + +interface JsonVisitor { + ( + current: JsonObject | JsonArray, + pointer: string, + parentSchema?: JsonObject | JsonArray, + index?: string, + ): void; +} + + +function _visitJsonSchema(schema: JsonObject, visitor: JsonVisitor) { + const keywords = { + additionalItems: true, + items: true, + contains: true, + additionalProperties: true, + propertyNames: true, + not: true, + }; + + const propsKeywords = { + definitions: true, + properties: true, + patternProperties: true, + dependencies: true, + }; + + function _traverse( + schema: JsonObject | JsonArray, + jsonPtr: string, + rootSchema: JsonObject, + parentSchema?: JsonObject | JsonArray, + keyIndex?: string, + ) { + if (schema && typeof schema == 'object' && !Array.isArray(schema)) { + visitor(schema, jsonPtr, parentSchema, keyIndex); + + for (const key of Object.keys(schema)) { + const sch = schema[key]; + if (Array.isArray(sch)) { + if (key == 'items') { + for (let i = 0; i < sch.length; i++) { + _traverse( + sch[i] as JsonArray, + jsonPtr + '/' + key + '/' + i, + rootSchema, + schema, + '' + i, + ); + } + } + } else if (key in propsKeywords) { + if (sch && typeof sch == 'object') { + for (const prop of Object.keys(sch)) { + _traverse( + sch[prop] as JsonObject, + jsonPtr + '/' + key + '/' + prop.replace(/~/g, '~0').replace(/\//g, '~1'), + rootSchema, + schema, + prop, + ); + } + } + } else if (key in keywords) { + _traverse(sch as JsonObject, jsonPtr + '/' + key, rootSchema, schema, key); + } + } + } + } + + _traverse(schema, '', schema); +} + + +export class AjvSchemaRegistry implements OptionsSchemaRegistry { + private _ajv: ajv.Ajv; + private _uriCache = new Map(); + + constructor() { + /** + * Build an AJV instance that will be used to validate schemas. + */ + this._ajv = ajv({ + removeAdditional: 'all', + useDefaults: true, + loadSchema: (uri: string) => this._fetch(uri) as ajv.Thenable, + }); + + this._ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); + } + + private _clean( + data: any, // tslint:disable-line:no-any + schema: JsonObject, + validate: ajv.ValidateFunction, + parentDataCache: WeakMap, // tslint:disable-line:no-any + ) { + _visitJsonSchema( + schema, + (currentSchema: object, pointer: string, parentSchema?: object, index?: string) => { + // If we're at the root, skip. + if (parentSchema === undefined || index === undefined) { + return; + } + + const parsedPointer = _parseJsonPointer(pointer); + // Every other path fragment is either 'properties', 'items', 'allOf', ... + const nonPropertyParsedPP = parsedPointer.filter((_, i) => !(i % 2)); + // Skip if it's part of a definitions or too complex for us to analyze. + if (nonPropertyParsedPP.some(f => f == 'definitions' || f == 'allOf' || f == 'anyOf')) { + return; + } + + let maybeParentData = parentDataCache.get(parentSchema); + if (!maybeParentData) { + // Every other path fragment is either 'properties' or 'items' in this model. + const parentDataPointer = parsedPointer.filter((_, i) => i % 2); + + // Find the parentData from the list. + maybeParentData = data; + for (const index of parentDataPointer.slice(0, -1)) { + if (maybeParentData[index] === undefined) { + // tslint:disable-next-line:no-any + if (parentSchema.hasOwnProperty('items') || (parentSchema as any)['type'] == 'array') { + maybeParentData[index] = []; + } else { + maybeParentData[index] = {}; + } + } + maybeParentData = maybeParentData[index]; + } + parentDataCache.set(parentSchema, maybeParentData); + } + + if (currentSchema.hasOwnProperty('$ref')) { + const $ref = (currentSchema as { $ref: string })['$ref']; + const refHash = $ref.split('#', 2)[1]; + const refUrl = $ref.startsWith('#') ? $ref : $ref.split('#', 1); + + let refVal = validate; + if (!$ref.startsWith('#')) { + // tslint:disable-next-line:no-any + refVal = (validate.refVal as any)[(validate.refs as any)[refUrl[0]]]; + } + if (refHash) { + // tslint:disable-next-line:no-any + refVal = (refVal.refVal as any)[(refVal.refs as any)['#' + refHash]]; + } + + maybeParentData[index] = {}; + this._clean(maybeParentData[index], refVal.schema as JsonObject, refVal, parentDataCache); + + return; + } else if (!maybeParentData.hasOwnProperty(index)) { + maybeParentData[index] = undefined; + } + }); + } + + private _fetch(uri: string): Promise { + const maybeSchema = this._uriCache.get(uri); + + if (maybeSchema) { + return Promise.resolve(maybeSchema); + } + + return new Promise((resolve, reject) => { + http.get(uri, res => { + if (!res.statusCode || res.statusCode >= 300) { + // Consume the rest of the data to free memory. + res.resume(); + reject(`Request failed. Status Code: ${res.statusCode}`); + } else { + res.setEncoding('utf8'); + let data = ''; + res.on('data', chunk => { + data += chunk; + }); + res.on('end', () => { + try { + const json = JSON.parse(data); + this._uriCache.set(uri, json); + resolve(json); + } catch (err) { + reject(err); + } + }); + } + }); + }); + } + + compile(schema: Object): Observable { + // Supports both synchronous and asynchronous compilation, by trying the synchronous + // version first, then if refs are missing this will fails. + // We also add any refs from external fetched schemas so that those will also be used + // in synchronous (if available). + let validator: Observable; + try { + const maybeFnValidate = this._ajv.compile(schema); + validator = Observable.of(maybeFnValidate); + } catch (e) { + // Propagate the error. + if (!(e instanceof (ajv.MissingRefError as {} as Function))) { + throw e; + } + + validator = new Observable(obs => { + this._ajv.compileAsync(schema) + .then(validate => { + obs.next(validate); + obs.complete(); + }, err => { + obs.error(err); + }); + }); + } + + return validator + .pipe( + // tslint:disable-next-line:no-any + map(validate => (data: any): Observable => { + const result = validate(data); + const resultObs = typeof result == 'boolean' + ? Observable.of(result) + : fromPromise(result as PromiseLike); + + return resultObs + .pipe( + map(result => { + if (result) { + // tslint:disable-next-line:no-any + const schemaDataMap = new WeakMap(); + schemaDataMap.set(schema, data); + + this._clean(data, schema as JsonObject, validate, schemaDataMap); + + return { success: true } as OptionsSchemaValidatorResult; + } + + return { + success: false, + errors: (validate.errors || []).map((err: ajv.ErrorObject) => err.message), + } as OptionsSchemaValidatorResult; + }), + ); + }), + ); + } +} diff --git a/packages/angular_devkit/schematics/tools/ajv-option-transform_spec.ts b/packages/angular_devkit/schematics/tools/ajv-option-transform_spec.ts new file mode 100644 index 0000000000..3c4978462f --- /dev/null +++ b/packages/angular_devkit/schematics/tools/ajv-option-transform_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 + */ +// tslint:disable:no-any +import 'rxjs/add/operator/mergeMap'; +import { AjvSchemaRegistry } from './ajv-option-transform'; + + +describe('AjvSchemaRegistry', () => { + it('works asynchronously', done => { + const registry = new AjvSchemaRegistry(); + const data: any = {}; // tslint:disable:no-any + + registry + .compile({ + properties: { + bool: { type: 'boolean' }, + str: { type: 'string', default: 'someString' }, + obj: { + properties: { + num: { type: 'number' }, + other: { type: 'number', default: 0 }, + }, + }, + tslint: { + $ref: 'http://json.schemastore.org/tslint#', + }, + }, + }) + .mergeMap(validator => validator(data)) + .map(result => { + expect(result.success).toBe(true); + expect(data.obj.num).toBeUndefined(); + expect(data.tslint).not.toBeUndefined(); + }) + .subscribe(done, done.fail); + }); + + // Synchronous failure is only used internally. + // If it's meant to be used externally then this test should change to truly be synchronous + // (i.e. not relyign on the observable). + it('works synchronously', done => { + const registry = new AjvSchemaRegistry(); + const data: any = {}; // tslint:disable:no-any + let isDone = false; + + registry + .compile({ + properties: { + bool: { type: 'boolean' }, + str: { type: 'string', default: 'someString' }, + obj: { + properties: { + num: { type: 'number' }, + other: { type: 'number', default: 0 }, + }, + }, + }, + }) + .mergeMap(validator => validator(data)) + .map(result => { + expect(result.success).toBe(true); + expect(data.obj.num).toBeUndefined(); + }) + .subscribe(() => { + isDone = true; + }, done.fail); + + expect(isDone).toBe(true); + done(); + }); +}); diff --git a/packages/angular_devkit/schematics/tools/fallback-engine-host.ts b/packages/angular_devkit/schematics/tools/fallback-engine-host.ts index b7eec28015..ffd7704eeb 100644 --- a/packages/angular_devkit/schematics/tools/fallback-engine-host.ts +++ b/packages/angular_devkit/schematics/tools/fallback-engine-host.ts @@ -13,6 +13,8 @@ import { Source, TypedSchematicContext, UnknownCollectionException, } from '@angular-devkit/schematics'; +import { Observable } from 'rxjs/Observable'; +import { mergeMap } from 'rxjs/operators/mergeMap'; import { Url } from 'url'; @@ -26,7 +28,7 @@ export type FallbackSchematicDescription = { export declare type OptionTransform = ( schematic: SchematicDescription, options: T, -) => R; +) => Observable; /** @@ -35,7 +37,6 @@ export declare type OptionTransform = ( */ export class FallbackEngineHost implements EngineHost<{}, {}> { private _hosts: EngineHost<{}, {}>[] = []; - private _transforms: OptionTransform[] = []; constructor() {} @@ -45,10 +46,6 @@ export class FallbackEngineHost implements EngineHost<{}, {}> { this._hosts.push(host); } - registerOptionsTransform(t: OptionTransform) { - this._transforms.push(t); - } - createCollectionDescription(name: string): CollectionDescription { for (const host of this._hosts) { try { @@ -87,8 +84,10 @@ export class FallbackEngineHost implements EngineHost<{}, {}> { transformOptions( schematic: SchematicDescription, options: OptionT, - ): ResultT { - return this._transforms.reduce((acc: ResultT, t) => t(schematic, acc), options) as ResultT; + ): Observable { + return (Observable.of(options) + .pipe(...this._hosts.map(host => mergeMap(opt => host.transformOptions(schematic, opt)))) + ) as {} as Observable; } listSchematicNames(collection: CollectionDescription): string[] { diff --git a/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts b/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts index 037e45b750..89605833de 100644 --- a/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts +++ b/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts @@ -14,6 +14,8 @@ import { UnknownSchematicException, } from '@angular-devkit/schematics'; import { dirname, isAbsolute, join, resolve } from 'path'; +import { Observable } from 'rxjs/Observable'; +import { mergeMap } from 'rxjs/operators/mergeMap'; import { Url } from 'url'; import { FileSystemCollectionDesc, @@ -27,7 +29,7 @@ import { readJsonFile } from './file-system-utility'; export declare type OptionTransform - = (schematic: FileSystemSchematicDescription, options: T) => R; + = (schematic: FileSystemSchematicDescription, options: T) => Observable; export class CollectionCannotBeResolvedException extends BaseException { @@ -230,8 +232,12 @@ export abstract class FileSystemEngineHostBase implements } transformOptions( - schematic: FileSystemSchematicDesc, options: OptionT): ResultT { - return this._transforms.reduce((acc: ResultT, t) => t(schematic, acc), options) as ResultT; + schematic: FileSystemSchematicDesc, + options: OptionT, + ): Observable { + return (Observable.of(options) + .pipe(...this._transforms.map(tFn => mergeMap(opt => tFn(schematic, opt)))) + ) as {} as Observable; } getSchematicRuleFactory( diff --git a/packages/angular_devkit/schematics/tools/index.ts b/packages/angular_devkit/schematics/tools/index.ts index 8df237b12f..d2fac31bff 100644 --- a/packages/angular_devkit/schematics/tools/index.ts +++ b/packages/angular_devkit/schematics/tools/index.ts @@ -13,3 +13,6 @@ export { FallbackEngineHost } from './fallback-engine-host'; export { FileSystemEngineHost } from './file-system-engine-host'; export { NodeModulesEngineHost } from './node-module-engine-host'; export { NodeModulesTestEngineHost } from './node-modules-test-engine-host'; + +export { AjvSchemaRegistry } from './ajv-option-transform'; +export { validateOptionsWithSchema } from './schema-option-transform'; diff --git a/packages/angular_devkit/schematics/tools/schema-option-transform.ts b/packages/angular_devkit/schematics/tools/schema-option-transform.ts new file mode 100644 index 0000000000..d3136c484a --- /dev/null +++ b/packages/angular_devkit/schematics/tools/schema-option-transform.ts @@ -0,0 +1,84 @@ +/** + * @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 { BaseException } from '@angular-devkit/core'; +import { SchematicDescription } from '@angular-devkit/schematics'; +import { Observable } from 'rxjs/Observable'; +import { first } from 'rxjs/operators/first'; +import { map } from 'rxjs/operators/map'; +import { mergeMap } from 'rxjs/operators/mergeMap'; +import { FileSystemCollectionDescription, FileSystemSchematicDescription } from './description'; + +export type SchematicDesc = + SchematicDescription; + + +export class InvalidInputOptions extends BaseException { + // tslint:disable-next-line:no-any + constructor(options: any, errors: string[]) { + super(`Schematic input does not validate against the Schema: ${JSON.stringify(options)}\n` + + `Errors:\n ${errors.join('\n ')}`); + } +} + + +export interface OptionsSchemaValidatorResult { + success: boolean; + errors?: string[]; +} + +export interface OptionsSchemaValidator { + // tslint:disable-next-line:no-any + (data: any): Observable; +} + +export interface OptionsSchemaRegistry { + compile(schema: Object): Observable; +} + +// tslint:disable-next-line:no-any +function _deepCopy(object: T): T { + const copy = {} as T; + for (const key of Object.keys(object)) { + if (typeof object[key] == 'object') { + copy[key] = _deepCopy(object[key]); + break; + } else { + copy[key] = object[key]; + } + } + + return copy; +} + + +// This can only be used in NodeJS. +export function validateOptionsWithSchema(registry: OptionsSchemaRegistry) { + return (schematic: SchematicDesc, options: T): Observable => { + // Prevent a schematic from changing the options object by making a copy of it. + options = _deepCopy(options); + + if (schematic.schema && schematic.schemaJson) { + // Make a deep copy of options. + return registry + .compile(schematic.schemaJson) + .pipe( + mergeMap(validator => validator(options)), + first(), + map(result => { + if (!result.success) { + throw new InvalidInputOptions(options, result.errors || ['Unknown reason.']); + } + + return options; + }), + ); + } + + return Observable.of(options); + }; +} diff --git a/packages/angular_devkit/schematics_cli/bin/schematics.ts b/packages/angular_devkit/schematics_cli/bin/schematics.ts index ed0ad9047a..b1f5b632fe 100644 --- a/packages/angular_devkit/schematics_cli/bin/schematics.ts +++ b/packages/angular_devkit/schematics_cli/bin/schematics.ts @@ -17,9 +17,10 @@ import { Tree, } from '@angular-devkit/schematics'; import { + AjvSchemaRegistry, FileSystemHost, - FileSystemSchematicDesc, NodeModulesEngineHost, + validateOptionsWithSchema, } from '@angular-devkit/schematics/tools'; import * as minimist from 'minimist'; import { Observable } from 'rxjs/Observable'; @@ -122,22 +123,9 @@ const isLocalCollection = collectionName.startsWith('.') || collectionName.start const engineHost = new NodeModulesEngineHost(); const engine = new SchematicEngine(engineHost); -const schemaRegistry = new schema.JsonSchemaRegistry(); // Add support for schemaJson. -engineHost.registerOptionsTransform((schematic: FileSystemSchematicDesc, options: {}) => { - if (schematic.schema && schematic.schemaJson) { - const schemaJson = schematic.schemaJson as schema.JsonSchemaObject; - const ref = schemaJson.$id || ('/' + schematic.collection.name + '/' + schematic.name); - schemaRegistry.addSchema(ref, schemaJson); - const serializer = new schema.serializers.JavascriptSerializer(); - const fn = serializer.serialize(ref, schemaRegistry); - - return fn(options); - } - - return options; -}); +engineHost.registerOptionsTransform(validateOptionsWithSchema(new AjvSchemaRegistry())); /** From a96ed3f18aa4d58bc5150a2f253e941ce11427fc Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Mon, 18 Dec 2017 13:32:32 +0000 Subject: [PATCH 03/10] feat(@angular-devkit/core): move AJV schema validation to core --- packages/angular_devkit/core/package.json | 1 + .../core/src/json/schema/index.ts | 11 +- .../core/src/json/schema/interface.ts | 25 ++ .../core/src/json/schema/registry.ts | 268 ++++++++++++++++- .../src/json/schema/registry_spec.ts} | 8 +- .../core/src/json/schema/schema.ts | 109 ------- .../src/json/schema/serializers/interface.ts | 13 - .../src/json/schema/serializers/javascript.ts | 105 ------- .../serializers/javascript_benchmark.ts | 52 ---- .../schema/serializers/serializers_spec.ts | 46 --- .../serializers/templates/javascript/index.ts | 21 -- .../templates/javascript/prop-any.ejs | 14 - .../templates/javascript/prop-array.ejs | 122 -------- .../templates/javascript/prop-boolean.ejs | 22 -- .../templates/javascript/prop-number.ejs | 47 --- .../templates/javascript/prop-object.ejs | 153 ---------- .../templates/javascript/prop-string.ejs | 39 --- .../serializers/templates/javascript/root.ejs | 20 -- .../templates/javascript/subschema.ejs | 38 --- .../angular_devkit/schematics/package.json | 3 +- .../testing/schematic-test-runner.ts | 6 +- .../schematics/tools/ajv-option-transform.ts | 278 ------------------ .../angular_devkit/schematics/tools/index.ts | 1 - .../tools/schema-option-transform.ts | 21 +- .../schematics_cli/bin/schematics.ts | 22 +- 25 files changed, 301 insertions(+), 1144 deletions(-) create mode 100644 packages/angular_devkit/core/src/json/schema/interface.ts rename packages/angular_devkit/{schematics/tools/ajv-option-transform_spec.ts => core/src/json/schema/registry_spec.ts} (91%) delete mode 100644 packages/angular_devkit/core/src/json/schema/schema.ts delete mode 100644 packages/angular_devkit/core/src/json/schema/serializers/interface.ts delete mode 100644 packages/angular_devkit/core/src/json/schema/serializers/javascript.ts delete mode 100644 packages/angular_devkit/core/src/json/schema/serializers/javascript_benchmark.ts delete mode 100644 packages/angular_devkit/core/src/json/schema/serializers/serializers_spec.ts delete mode 100644 packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/index.ts delete mode 100644 packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-any.ejs delete mode 100644 packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-array.ejs delete mode 100644 packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-boolean.ejs delete mode 100644 packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-number.ejs delete mode 100644 packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-object.ejs delete mode 100644 packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-string.ejs delete mode 100644 packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/root.ejs delete mode 100644 packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/subschema.ejs delete mode 100644 packages/angular_devkit/schematics/tools/ajv-option-transform.ts diff --git a/packages/angular_devkit/core/package.json b/packages/angular_devkit/core/package.json index 946165a90b..fb7a470a19 100644 --- a/packages/angular_devkit/core/package.json +++ b/packages/angular_devkit/core/package.json @@ -11,6 +11,7 @@ "core" ], "dependencies": { + "ajv": "~5.5.1", "chokidar": "^1.7.0", "source-map": "^0.5.6" } diff --git a/packages/angular_devkit/core/src/json/schema/index.ts b/packages/angular_devkit/core/src/json/schema/index.ts index b0120254d0..3eabe58ebc 100644 --- a/packages/angular_devkit/core/src/json/schema/index.ts +++ b/packages/angular_devkit/core/src/json/schema/index.ts @@ -5,14 +5,5 @@ * 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 * as javascript from './serializers/javascript'; - +export * from './interface'; export * from './registry'; -export * from './schema'; - - -export { javascript }; - -export const serializers = { - JavascriptSerializer: javascript.JavascriptSerializer, -}; diff --git a/packages/angular_devkit/core/src/json/schema/interface.ts b/packages/angular_devkit/core/src/json/schema/interface.ts new file mode 100644 index 0000000000..a634354046 --- /dev/null +++ b/packages/angular_devkit/core/src/json/schema/interface.ts @@ -0,0 +1,25 @@ +/** + * @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 { Observable } from 'rxjs/Observable'; + + +export interface SchemaValidatorResult { + success: boolean; + errors?: string[]; +} + + +export interface SchemaValidator { + // tslint:disable-next-line:no-any + (data: any): Observable; +} + + +export interface SchemaRegistry { + compile(schema: Object): Observable; +} diff --git a/packages/angular_devkit/core/src/json/schema/registry.ts b/packages/angular_devkit/core/src/json/schema/registry.ts index 703f68c8bd..4caeeda6bc 100644 --- a/packages/angular_devkit/core/src/json/schema/registry.ts +++ b/packages/angular_devkit/core/src/json/schema/registry.ts @@ -5,35 +5,271 @@ * 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 { BaseException } from '../../exception/exception'; -import { JsonSchema } from './schema'; +import * as ajv from 'ajv'; +import * as http from 'http'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; +import { fromPromise } from 'rxjs/observable/fromPromise'; +import { map } from 'rxjs/operators/map'; +import { JsonArray, JsonObject } from '../interface'; +import { SchemaRegistry, SchemaValidator, SchemaValidatorResult } from './interface'; -export class JsonSchemaNotFoundException extends BaseException { - constructor(ref: string) { super(`Reference "${ref}" could not be found in registry.`); } +function _parseJsonPointer(pointer: string): string[] { + if (pointer === '') { return []; } + if (pointer.charAt(0) !== '/') { throw new Error('Invalid JSON pointer: ' + pointer); } + + return pointer.substring(1).split(/\//).map(str => str.replace(/~1/g, '/').replace(/~0/g, '~')); +} + + +interface JsonVisitor { + ( + current: JsonObject | JsonArray, + pointer: string, + parentSchema?: JsonObject | JsonArray, + index?: string, + ): void; } -export class JsonSchemaRegistry { - private _cache = new Map(); +function _visitJsonSchema(schema: JsonObject, visitor: JsonVisitor) { + const keywords = { + additionalItems: true, + items: true, + contains: true, + additionalProperties: true, + propertyNames: true, + not: true, + }; - constructor() {} + const propsKeywords = { + definitions: true, + properties: true, + patternProperties: true, + dependencies: true, + }; - addSchema(ref: string, schema: JsonSchema) { - this._cache.set(ref, schema); + function _traverse( + schema: JsonObject | JsonArray, + jsonPtr: string, + rootSchema: JsonObject, + parentSchema?: JsonObject | JsonArray, + keyIndex?: string, + ) { + if (schema && typeof schema == 'object' && !Array.isArray(schema)) { + visitor(schema, jsonPtr, parentSchema, keyIndex); + + for (const key of Object.keys(schema)) { + const sch = schema[key]; + if (Array.isArray(sch)) { + if (key == 'items') { + for (let i = 0; i < sch.length; i++) { + _traverse( + sch[i] as JsonArray, + jsonPtr + '/' + key + '/' + i, + rootSchema, + schema, + '' + i, + ); + } + } + } else if (key in propsKeywords) { + if (sch && typeof sch == 'object') { + for (const prop of Object.keys(sch)) { + _traverse( + sch[prop] as JsonObject, + jsonPtr + '/' + key + '/' + prop.replace(/~/g, '~0').replace(/\//g, '~1'), + rootSchema, + schema, + prop, + ); + } + } + } else if (key in keywords) { + _traverse(sch as JsonObject, jsonPtr + '/' + key, rootSchema, schema, key); + } + } + } } - hasSchema(ref: string) { - return this._cache.has(ref); + _traverse(schema, '', schema); +} + + +export class CoreSchemaRegistry implements SchemaRegistry { + private _ajv: ajv.Ajv; + private _uriCache = new Map(); + + constructor() { + /** + * Build an AJV instance that will be used to validate schemas. + */ + this._ajv = ajv({ + removeAdditional: 'all', + useDefaults: true, + loadSchema: (uri: string) => this._fetch(uri) as ajv.Thenable, + }); + + this._ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); } - getSchemaFromRef(ref: string): JsonSchema { - const schemaCache = this._cache.get(ref); + private _clean( + data: any, // tslint:disable-line:no-any + schema: JsonObject, + validate: ajv.ValidateFunction, + parentDataCache: WeakMap, // tslint:disable-line:no-any + ) { + _visitJsonSchema( + schema, + (currentSchema: object, pointer: string, parentSchema?: object, index?: string) => { + // If we're at the root, skip. + if (parentSchema === undefined || index === undefined) { + return; + } + + const parsedPointer = _parseJsonPointer(pointer); + // Every other path fragment is either 'properties', 'items', 'allOf', ... + const nonPropertyParsedPP = parsedPointer.filter((_, i) => !(i % 2)); + // Skip if it's part of a definitions or too complex for us to analyze. + if (nonPropertyParsedPP.some(f => f == 'definitions' || f == 'allOf' || f == 'anyOf')) { + return; + } + + let maybeParentData = parentDataCache.get(parentSchema); + if (!maybeParentData) { + // Every other path fragment is either 'properties' or 'items' in this model. + const parentDataPointer = parsedPointer.filter((_, i) => i % 2); + + // Find the parentData from the list. + maybeParentData = data; + for (const index of parentDataPointer.slice(0, -1)) { + if (maybeParentData[index] === undefined) { + // tslint:disable-next-line:no-any + if (parentSchema.hasOwnProperty('items') || (parentSchema as any)['type'] == 'array') { + maybeParentData[index] = []; + } else { + maybeParentData[index] = {}; + } + } + maybeParentData = maybeParentData[index]; + } + parentDataCache.set(parentSchema, maybeParentData); + } + + if (currentSchema.hasOwnProperty('$ref')) { + const $ref = (currentSchema as { $ref: string })['$ref']; + const refHash = $ref.split('#', 2)[1]; + const refUrl = $ref.startsWith('#') ? $ref : $ref.split('#', 1); - if (!schemaCache) { - throw new JsonSchemaNotFoundException(ref); + let refVal = validate; + if (!$ref.startsWith('#')) { + // tslint:disable-next-line:no-any + refVal = (validate.refVal as any)[(validate.refs as any)[refUrl[0]]]; + } + if (refHash) { + // tslint:disable-next-line:no-any + refVal = (refVal.refVal as any)[(refVal.refs as any)['#' + refHash]]; + } + + maybeParentData[index] = {}; + this._clean(maybeParentData[index], refVal.schema as JsonObject, refVal, parentDataCache); + + return; + } else if (!maybeParentData.hasOwnProperty(index)) { + maybeParentData[index] = undefined; + } + }); + } + + private _fetch(uri: string): Promise { + const maybeSchema = this._uriCache.get(uri); + + if (maybeSchema) { + return Promise.resolve(maybeSchema); + } + + return new Promise((resolve, reject) => { + http.get(uri, res => { + if (!res.statusCode || res.statusCode >= 300) { + // Consume the rest of the data to free memory. + res.resume(); + reject(`Request failed. Status Code: ${res.statusCode}`); + } else { + res.setEncoding('utf8'); + let data = ''; + res.on('data', chunk => { + data += chunk; + }); + res.on('end', () => { + try { + const json = JSON.parse(data); + this._uriCache.set(uri, json); + resolve(json); + } catch (err) { + reject(err); + } + }); + } + }); + }); + } + + compile(schema: Object): Observable { + // Supports both synchronous and asynchronous compilation, by trying the synchronous + // version first, then if refs are missing this will fails. + // We also add any refs from external fetched schemas so that those will also be used + // in synchronous (if available). + let validator: Observable; + try { + const maybeFnValidate = this._ajv.compile(schema); + validator = Observable.of(maybeFnValidate); + } catch (e) { + // Propagate the error. + if (!(e instanceof (ajv.MissingRefError as {} as Function))) { + throw e; + } + + validator = new Observable(obs => { + this._ajv.compileAsync(schema) + .then(validate => { + obs.next(validate); + obs.complete(); + }, err => { + obs.error(err); + }); + }); } - return schemaCache; + return validator + .pipe( + // tslint:disable-next-line:no-any + map(validate => (data: any): Observable => { + const result = validate(data); + const resultObs = typeof result == 'boolean' + ? Observable.of(result) + : fromPromise(result as PromiseLike); + + return resultObs + .pipe( + map(result => { + if (result) { + // tslint:disable-next-line:no-any + const schemaDataMap = new WeakMap(); + schemaDataMap.set(schema, data); + + this._clean(data, schema as JsonObject, validate, schemaDataMap); + + return { success: true } as SchemaValidatorResult; + } + + return { + success: false, + errors: (validate.errors || []).map((err: ajv.ErrorObject) => err.message), + } as SchemaValidatorResult; + }), + ); + }), + ); } } diff --git a/packages/angular_devkit/schematics/tools/ajv-option-transform_spec.ts b/packages/angular_devkit/core/src/json/schema/registry_spec.ts similarity index 91% rename from packages/angular_devkit/schematics/tools/ajv-option-transform_spec.ts rename to packages/angular_devkit/core/src/json/schema/registry_spec.ts index 3c4978462f..917cf6bde8 100644 --- a/packages/angular_devkit/schematics/tools/ajv-option-transform_spec.ts +++ b/packages/angular_devkit/core/src/json/schema/registry_spec.ts @@ -7,12 +7,12 @@ */ // tslint:disable:no-any import 'rxjs/add/operator/mergeMap'; -import { AjvSchemaRegistry } from './ajv-option-transform'; +import { CoreSchemaRegistry } from './registry'; -describe('AjvSchemaRegistry', () => { +describe('CoreSchemaRegistry', () => { it('works asynchronously', done => { - const registry = new AjvSchemaRegistry(); + const registry = new CoreSchemaRegistry(); const data: any = {}; // tslint:disable:no-any registry @@ -44,7 +44,7 @@ describe('AjvSchemaRegistry', () => { // If it's meant to be used externally then this test should change to truly be synchronous // (i.e. not relyign on the observable). it('works synchronously', done => { - const registry = new AjvSchemaRegistry(); + const registry = new CoreSchemaRegistry(); const data: any = {}; // tslint:disable:no-any let isDone = false; diff --git a/packages/angular_devkit/core/src/json/schema/schema.ts b/packages/angular_devkit/core/src/json/schema/schema.ts deleted file mode 100644 index 99e08886dd..0000000000 --- a/packages/angular_devkit/core/src/json/schema/schema.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * @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 { JsonArray, JsonObject, JsonValue } from '../interface'; - - -export interface JsonSchemaBase extends Partial { - $schema?: string; - $id?: string; - - // Metadata. - id?: string; - title?: string; - description?: string; - readonly?: boolean; - - // Reference properties. - $ref?: string; - allOf?: (JsonObject & JsonSchema)[]; - anyOf?: (JsonObject & JsonSchema)[]; - oneOf?: (JsonObject & JsonSchema)[]; - - // Structural properties. - definitions?: { [name: string]: (JsonObject & JsonSchema) }; -} - -export interface JsonSchemaString { - type: 'string'; - default?: string; - - minLength?: number; - maxLength?: number; - pattern?: string; - format?: string; -} - -export interface JsonSchemaNumberBase { - default?: number; - multipleOf?: number; - - // Range. - minimum?: number; - maximum?: number; - exclusiveMinimum?: boolean; - exclusiveMaximum?: boolean; -} - -export interface JsonSchemaNumber extends JsonSchemaNumberBase { - type: 'number'; -} - -export interface JsonSchemaInteger extends JsonSchemaNumberBase { - type: 'integer'; -} - -export interface JsonSchemaBoolean { - type: 'boolean'; - default?: boolean; -} - -export interface JsonSchemaObject extends JsonSchemaBase { - type: 'object'; - - // Object properties. - properties?: { [name: string]: (JsonObject & JsonSchema) }; - patternProperties?: { [pattern: string]: (JsonObject & JsonSchema) }; - required?: string[]; - minProperties?: number; - maxProperties?: number; - - dependencies?: { [name: string]: (string & JsonSchema) | string[]; }; - - additionalProperties?: boolean | (JsonObject & JsonSchema); -} - -export interface JsonSchemaArray extends JsonSchemaBase { - type: 'array'; - - additionalItems?: boolean | (JsonObject & JsonSchema); - items?: JsonArray; - maxItems?: number; - minItems?: number; - uniqueItems?: boolean; -} - -export interface JsonSchemaOneOfType extends JsonSchemaBase { - type: JsonArray & (JsonSchemaBaseType[]); -} - -export interface JsonSchemaAny { - // Type related properties. - type: undefined; - default?: JsonValue; - enum?: JsonArray; -} - - -export type JsonSchema = JsonSchemaString - | JsonSchemaNumber - | JsonSchemaInteger - | JsonSchemaObject - | JsonSchemaArray - | JsonSchemaOneOfType - | JsonSchemaAny; -export type JsonSchemaBaseType = undefined | 'string' | 'number' | 'object' | 'array' | 'boolean'; diff --git a/packages/angular_devkit/core/src/json/schema/serializers/interface.ts b/packages/angular_devkit/core/src/json/schema/serializers/interface.ts deleted file mode 100644 index a1bd731ca2..0000000000 --- a/packages/angular_devkit/core/src/json/schema/serializers/interface.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @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 { JsonSchemaRegistry } from '../registry'; - - -export abstract class JsonSchemaSerializer { - abstract serialize(ref: string, registry: JsonSchemaRegistry): T; -} diff --git a/packages/angular_devkit/core/src/json/schema/serializers/javascript.ts b/packages/angular_devkit/core/src/json/schema/serializers/javascript.ts deleted file mode 100644 index 53769cea4d..0000000000 --- a/packages/angular_devkit/core/src/json/schema/serializers/javascript.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * @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 { BaseException } from '../../../exception/exception'; -import { camelize, classify } from '../../../utils/strings'; -import { JsonSchemaRegistry } from '../registry'; -import { JsonSchema } from '../schema'; -import { JsonSchemaSerializer } from './interface'; - - -export class InvalidRangeException extends BaseException { - constructor(name: string, value: T, comparator: string, expected: T) { - super(`Property ${JSON.stringify(name)} expected a value ` - + `${comparator} ${JSON.stringify(expected)}, received ${JSON.stringify(value)}.`); - } -} -export class InvalidValueException extends BaseException { - constructor(name: string, value: {}, expected: string) { - super(`Property ${JSON.stringify(name)} expected a value of type ${expected}, ` - + `received ${value}.`); - } -} -export class InvalidSchemaException extends BaseException { - constructor(schema: JsonSchema) { - super(`Invalid schema: ${JSON.stringify(schema)}`); - } -} -export class InvalidPropertyNameException extends BaseException { - constructor(public readonly path: string) { - super(`Property ${JSON.stringify(path)} does not exist in the schema, and no additional ` - + `properties are allowed.`); - } -} -export class RequiredValueMissingException extends BaseException { - constructor(public readonly path: string) { - super(`Property ${JSON.stringify(path)} is required but missing.`); - } -} - - -export const exceptions = { - InvalidRangeException, - InvalidSchemaException, - InvalidValueException, - InvalidPropertyNameException, - RequiredValueMissingException, -}; - - -const symbols = { - Schema: Symbol('schema'), -}; - - -export interface JavascriptSerializerOptions { - // Do not throw an exception if an extra property is passed, simply ignore it. - ignoreExtraProperties?: boolean; - // Allow accessing undefined objects, which might have default property values. - allowAccessUndefinedObjects?: boolean; -} - - -export class JavascriptSerializer extends JsonSchemaSerializer<(value: T) => T> { - private _uniqueSet = new Set(); - - constructor(private _options?: JavascriptSerializerOptions) { super(); } - - protected _unique(name: string) { - let i = 1; - let result = name; - while (this._uniqueSet.has(result)) { - result = name + i; - i++; - } - this._uniqueSet.add(result); - - return result; - } - - serialize(ref: string, registry: JsonSchemaRegistry) { - const rootSchema = registry.getSchemaFromRef(ref); - const { root, templates } = require('./templates/javascript'); - - const source = root({ - exceptions, - name: '', - options: this._options || {}, - schema: rootSchema, - strings: { - classify, - camelize, - }, - symbols, - templates, - }); - - const fn = new Function('registry', 'exceptions', 'symbols', 'value', source); - - return (value: T) => fn(registry, exceptions, symbols, value); - } -} diff --git a/packages/angular_devkit/core/src/json/schema/serializers/javascript_benchmark.ts b/packages/angular_devkit/core/src/json/schema/serializers/javascript_benchmark.ts deleted file mode 100644 index b38822b398..0000000000 --- a/packages/angular_devkit/core/src/json/schema/serializers/javascript_benchmark.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @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 - */ -// tslint:disable:no-any -import { benchmark } from '@_/benchmark'; -import { SchemaClassFactory } from '@ngtools/json-schema'; -import * as fs from 'fs'; -import * as path from 'path'; -import { JsonSchemaRegistry } from '../registry'; -import { JsonSchema } from '../schema'; -import { JavascriptSerializer } from './javascript'; - -describe('JavaScript Serializer', () => { - // Schema for the Angular-CLI config. - const jsonPath = path.join( - (global as any)._DevKitRoot, - 'tests/@angular_devkit/core/json/schema/serializers/schema_benchmark.json', - ); - const jsonContent = fs.readFileSync(jsonPath).toString(); - const complexSchema: JsonSchema = JSON.parse(jsonContent); - - const registry = new JsonSchemaRegistry(); - registry.addSchema('', complexSchema); - - benchmark('schema parsing', () => { - new JavascriptSerializer().serialize('', registry)({}); - }, () => { - const SchemaMetaClass = SchemaClassFactory(complexSchema); - const schemaClass = new SchemaMetaClass({}); - schemaClass.$$root(); - }); - - (function() { - const registry = new JsonSchemaRegistry(); - registry.addSchema('', complexSchema); - const coreRoot = new JavascriptSerializer().serialize('', registry)({}); - - const SchemaMetaClass = SchemaClassFactory(complexSchema); - const schemaClass = new SchemaMetaClass({}); - const ngtoolsRoot = schemaClass.$$root(); - - benchmark('schema access', () => { - coreRoot.project = { name: 'abc' }; - }, () => { - ngtoolsRoot.project = { name: 'abc' }; - }); - })(); -}); diff --git a/packages/angular_devkit/core/src/json/schema/serializers/serializers_spec.ts b/packages/angular_devkit/core/src/json/schema/serializers/serializers_spec.ts deleted file mode 100644 index 08cd8af44d..0000000000 --- a/packages/angular_devkit/core/src/json/schema/serializers/serializers_spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * @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 - */ -// tslint:disable:no-any -import * as fs from 'fs'; -import * as path from 'path'; -import { JsonSchemaRegistry } from '../registry'; - -describe('serializers', () => { - const devkitRoot = (global as any)._DevKitRoot; - const root = path.join(devkitRoot, 'tests/@angular_devkit/core/json/schema/serializers'); - const allFiles = fs.readdirSync(root); - const schemas = allFiles.filter(x => x.match(/^\d+\.schema\.json$/)); - - for (const schemaName of schemas) { - // tslint:disable-next-line:non-null-operator - const schemaN = schemaName.match(/^\d+/) ![0] || '0'; - const schema = JSON.parse(fs.readFileSync(path.join(root, schemaName)).toString()); - - const serializers = allFiles.filter(x => { - return x.startsWith(schemaN + '.') && x.match(/^\d+\.\d+\..*_spec\.[jt]s$/); - }); - - for (const serializerName of serializers) { - // tslint:disable-next-line:non-null-operator - const [, indexN, serializerN] = serializerName.match(/^\d+\.(\d+)\.(.*)_spec/) !; - const serializer = require(path.join(root, serializerName)); - - const registry = new JsonSchemaRegistry(); - - for (const fnName of Object.keys(serializer)) { - if (typeof serializer[fnName] != 'function') { - continue; - } - - it(`${JSON.stringify(serializerN)} (${schemaN}.${indexN}.${fnName})`, () => { - serializer[fnName](registry, schema); - }); - } - } - } -}); diff --git a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/index.ts b/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/index.ts deleted file mode 100644 index 13686cc191..0000000000 --- a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @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 - */ - -export const templates: { [name: string]: (value: {}) => string } = { - subschema: require('./subschema').default, - - prop_any: require('./prop-any').default, - prop_array: require('./prop-array').default, - prop_boolean: require('./prop-boolean').default, - prop_number: require('./prop-number').default, - prop_integer: require('./prop-number').default, - prop_object: require('./prop-object').default, - prop_string: require('./prop-string').default, -}; - -export const root = require('./root').default as (value: {}) => string; diff --git a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-any.ejs b/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-any.ejs deleted file mode 100644 index 66191f8333..0000000000 --- a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-any.ejs +++ /dev/null @@ -1,14 +0,0 @@ -{ - value: undefined, - get() { return this.value === undefined ? <%= 'default' in schema ? JSON.stringify(schema.default) : 'undefined' %> : this.value; }, - set(v) { - if (v === undefined && <%= !required %>) { - this.value = undefined; - return; - } - this.value = v; - }, - isDefined() { return this.value !== undefined; }, - remove() { this.set(undefined); }, - schema() { return <%= JSON.stringify(schema) %>; }, -} diff --git a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-array.ejs b/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-array.ejs deleted file mode 100644 index 18acac6729..0000000000 --- a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-array.ejs +++ /dev/null @@ -1,122 +0,0 @@ -(function() { -<% - if ('default' in schema && !Array.isArray(schema.default) - || 'minItems' in schema && (typeof schema.minItems != 'number' || schema.minItems < 0) - || 'maxItems' in schema && (typeof schema.maxItems != 'number' || schema.maxItems < 0) - || 'uniqueItems' in schema && typeof schema.uniqueItems != 'boolean') { - throw new exceptions.InvalidSchemaException(schema); - } - - const required = (schema.required || []); - const extras = { - exceptions: exceptions, - options: options, - path: path, - strings: strings, - symbols: symbols, - templates: templates, - }; -%> - -const itemHandler = function() { return (<%= - templates.subschema(Object.assign({ - name: '???', - required: false, - schema: schema.additionalProperties, - }, extras)) -%>); }; - - -const items = []; -const arrayFunctions = { - get length() { return items.length; }, - push() { items.push.apply(items, arguments); }, - pop() { return items.pop(); }, - shift() { return items.shift(); }, - unshift() { return items.unshift(); }, - slice(start, end) { return items.slice(start, end); }, -}; - - -let defined = false; -const proxy = new Proxy({}, { - isExtensible() { return false; }, - has(target, prop) { - return (prop in items); - }, - get(target, prop) { - if (prop === symbols.Schema) { - return arrayHandler.schema; - } - - if (prop >= 0 && prop in value) { - return value[prop].get(); - } - if (prop in arrayFunctions) { - return arrayFunctions[prop]; - } - return undefined; - }, - set(target, prop, v) { - if (prop >= 0) { - if (!(prop in items)) { - items[prop] = itemHandler(); - } - items[prop].set(v); - return true; - } - return false; - }, - deleteProperty(target, prop) { - if (prop >= 0 && prop in value) { - value[prop].remove(); - return true; - } - return false; - }, - defineProperty(target, prop, descriptor) { - return false; - }, - getOwnPropertyDescriptor(target, prop) { - if (prop >= 0 && prop in value) { - return { configurable: true, enumerable: true }; - } - }, - ownKeys(target) { - return Object.keys(items); - }, -}); - -const arrayHandler = { - set(v) { - if (v === undefined) { - defined = false; - return; - } - - defined = true; - for (const key of Object.keys(v)) { - proxy[key] = v[key]; - } - - // Validate required fields. - <% for (const key of required) { %> - if (!(<%= JSON.stringify(key) %> in v)) { - throw new exceptions.RequiredValueMissingException(<%= JSON.stringify(path) %> + '/' + <%= JSON.stringify(key) %>); - }<% } %> - }, - get() { - if (defined) { - return proxy; - } else { - return <%= 'default' in schema ? JSON.stringify(schema.default) : 'undefined' %>; - } - }, - isDefined() { return defined; }, - remove() { this.set(undefined); }, - schema: <%= JSON.stringify(schema) %>, -}; - -return arrayHandler; - -})() diff --git a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-boolean.ejs b/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-boolean.ejs deleted file mode 100644 index 1294e09863..0000000000 --- a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-boolean.ejs +++ /dev/null @@ -1,22 +0,0 @@ -{ -<% - if ('default' in schema && typeof schema.default != 'boolean') { - throw new exceptions.InvalidSchemaException(schema); - } -%> - value: undefined, - get() { return this.value === undefined ? <%= schema.default || 'undefined' %> : this.value; }, - set(v) { - if (v === undefined && <%= !required %>) { - this.value = undefined; - return; - } - if (typeof v != 'boolean') { - throw new exceptions.InvalidValueException(<%= JSON.stringify(name) %>, typeof v, 'boolean'); - } - this.value = v; - }, - isDefined() { return this.value !== undefined; }, - remove() { this.set(undefined); }, - schema() { return <%= JSON.stringify(schema) %>; }, -} diff --git a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-number.ejs b/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-number.ejs deleted file mode 100644 index 3b76609ec4..0000000000 --- a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-number.ejs +++ /dev/null @@ -1,47 +0,0 @@ -{ -<% - if ('default' in schema && typeof schema.default != 'number' - || 'minimum' in schema && typeof schema.minimum != 'number' - || 'maximum' in schema && typeof schema.maximum != 'number' - || 'multipleOf' in schema && typeof schema.multipleOf != 'number' - || (!('minimum' in schema) && 'exclusiveMinimum' in schema) - || (!('maximum' in schema) && 'exclusiveMaximum' in schema)) { - throw new exceptions.InvalidSchemaException(schema); - } -%> - value: undefined, - get() { return this.value === undefined ? <%= schema.default || 'undefined' %> : this.value; }, - set(v) { - if (v === undefined && <%= !required %>) { - this.value = undefined; - return; - } - if (typeof v != 'number') { - throw new exceptions.InvalidValueException(<%= JSON.stringify(name) %>, typeof v, 'number'); - }<% -if (schema.type == 'integer') { %> - if (v % 1 != 0) { - throw new exceptions.InvalidValueException(<%= JSON.stringify(name) %>, v, 'integer'); - }<% -} -if ('minimum' in schema) { %> - if (v <%= schema.exclusiveMinimum ? '<=' : '<' %> <%= schema.minimum %>) { - throw new exceptions.InvalidRangeException(<%= JSON.stringify(name) %>, v, '>=', <%= schema.minimum %>); - }<% -} -if ('maximum' in schema) { %> - if (v <%= schema.exclusiveMaximum ? '>=' : '>' %> <%= schema.maximum %>) { - throw new exceptions.InvalidRangeException(<%= JSON.stringify(name) %>, v, '>=', <%= schema.maximum %>); - }<% -} -if ('multipleOf' in schema) { %> - if (v % <%= schema.multipleOf %> != 0) { - throw new exceptions.InvalidRangeException(<%= JSON.stringify(name) %>, v, 'multiple of', <%= schema.maximum %>); - }<% -} %> - this.value = v; - }, - isDefined() { return this.value !== undefined; }, - remove() { this.set(undefined); }, - schema() { return <%= JSON.stringify(schema) %>; }, -} diff --git a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-object.ejs b/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-object.ejs deleted file mode 100644 index eb2412e3cb..0000000000 --- a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-object.ejs +++ /dev/null @@ -1,153 +0,0 @@ -(function() { -<% - const required = (schema.required || []); - const extras = { - exceptions: exceptions, - options: options, - path: path, - strings: strings, - symbols: symbols, - templates: templates, - }; -%> - -const additionalProperties = {}; -const additionalPropertyHandler = <% -if (!('additionalProperties' in schema)) { %>null<% } else { %> function() { return (<%= - templates.subschema(Object.assign({ - name: '???', - required: false, - schema: schema.additionalProperties, - }, extras)) %>); }<% -} -%>; - -const handlers = Object.create(null); -<% - for (const propName of Object.keys(schema.properties)) { - const _key = JSON.stringify(propName); - const _name = propName.match(/^[_a-zA-Z][_a-zA-Z0-9]*$/) ? propName : `[${_key}]`; -%>handlers[<%= JSON.stringify(_name) %>] = <%= - templates.subschema(Object.assign({ - name: _name, - required: required.indexOf(propName) != -1, - schema: schema.properties[propName], - }, extras)) -%><% -} -%> - -const objectFunctions = { - hasOwnProperty(name) { return objectProxyHandler.has(null, name); }, -}; - - -let defined = false; -const objectProxyHandler = { - isExtensible() { return false; }, - has(target, prop) { - return (prop in handlers && handlers[prop].isDefined()) - || (additionalPropertyHandler - ? (prop in additionalProperties && additionalProperties[prop].isDefined()) - : false); - }, - get(target, prop) { - if (prop === symbols.Schema) { - return objectHandler.schema; - } - if (prop in handlers) { - return handlers[prop].get(); - } - if (prop in objectFunctions) { - return objectFunctions[prop]; - } - return undefined; - }, - set(target, prop, v) { - defined = true; - if (prop in handlers) { - handlers[prop].set(v); - return true; - } else if (additionalPropertyHandler) { - if (!(prop in additionalProperties)) { - additionalProperties[prop] = additionalPropertyHandler(prop); - } - additionalProperties[prop].set(v); - return true; - } else { - <% if (options.ignoreExtraProperties !== true) { - %>throw new exceptions.InvalidPropertyNameException(<%= JSON.stringify(path) %> + '/' + prop);<% - } else { - // Just ignore the property. - %>return true;<% - } %> - } - }, - deleteProperty(target, prop) { - if (prop in handlers) { - handlers[prop].remove(); - return true; - } else if (additionalPropertyHandler && prop in additionalProperties) { - delete additionalProperties[prop]; - } - }, - defineProperty(target, prop, descriptor) { - return false; - }, - getOwnPropertyDescriptor(target, prop) { - if (prop in handlers) { - return { configurable: true, enumerable: true }; - } else if (additionalPropertyHandler && prop in additionalPropertyHandler) { - return { configurable: true, enumerable: true }; - } - }, - ownKeys(target) { - return [].concat( - Object.keys(handlers), - additionalPropertyHandler ? Object.keys(additionalProperties) : [] - ); - }, -}; - - -const proxy = new Proxy({}, objectProxyHandler); - -const objectHandler = { - set(v) { - if (v === undefined) { - defined = false; - return; - } - - defined = true; - for (const key of Object.keys(v)) { - proxy[key] = v[key]; - } - - // Validate required fields. - <% for (const key of required) { %> - if (!(<%= JSON.stringify(key) %> in v)) { - throw new exceptions.RequiredValueMissingException(<%= JSON.stringify(path) %> + '/' + <%= JSON.stringify(key) %>); - }<% } %> - }, - get() { - if (<%= options.allowAccessUndefinedObjects === true %> || this.isDefined()) { - return proxy; - } else { - return <%= 'default' in schema ? JSON.stringify(schema.default) : 'undefined' %>; - } - }, - isDefined() { - return defined - && (Object.keys(handlers).some(function(x) { return handlers[x].isDefined(); }) - || Object.keys(additionalProperties).some(function(x) { - return additionalProperties[x].isDefined(); - })); - }, - remove() { this.set(undefined); }, - schema: <%= JSON.stringify(schema) %>, -}; - -return objectHandler; - -})() diff --git a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-string.ejs b/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-string.ejs deleted file mode 100644 index 20b84a86bf..0000000000 --- a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/prop-string.ejs +++ /dev/null @@ -1,39 +0,0 @@ -{ -<% -if ('default' in schema && typeof schema.default != 'string' - || 'minLength' in schema && (typeof schema.minLength != 'number' || schema.minLength < 0) - || 'maxLength' in schema && (typeof schema.maxLength != 'number' || schema.maxLength < 0) - || 'pattern' in schema && typeof schema.pattern != 'string' - || 'format' in schema && typeof schema.format != 'string') { - throw new exceptions.InvalidSchemaException(schema); -} - -const pattern = ('pattern' in schema) ? new RegExp(schema.pattern) : null; -%> - value: undefined, - get() { return this.value === undefined ? <%= JSON.stringify(schema.default) || 'undefined' %> : this.value; }, - set(v) { - if (v === undefined && <%= !required %>) { - this.value = undefined; - return; - } - if (typeof v != 'string') { - throw new exceptions.InvalidValueException(<%= JSON.stringify(name) %>, typeof v, 'string'); - }<% -if ('minLength' in schema) { %> - if (v.length <= <%= schema.minLength %>) { - throw new exceptions.InvalidRangeException(<%= JSON.stringify(name) %>, v, 'longer', <%= schema.minLength %>); - }<% -} -if ('maxLength' in schema) { %> - if (v.length >= <%= schema.maxLength %>) { - throw new exceptions.InvalidRangeException(<%= JSON.stringify(name) %>, v, 'smaller', <%= schema.maxLength %>); - }<% -} -%> - this.value = v; - }, - isDefined() { return this.value !== undefined; }, - remove() { this.set(undefined); }, - schema() { return <%= JSON.stringify(schema) %>; }, -} diff --git a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/root.ejs b/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/root.ejs deleted file mode 100644 index 2e3f4f230d..0000000000 --- a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/root.ejs +++ /dev/null @@ -1,20 +0,0 @@ -<% -const extras = { - exceptions, - options, - strings, - symbols, - templates, -}; -%> - -const holder = <%= templates.subschema(Object.assign({ - name: name, - path: '', - required: false, - schema: schema, -}, extras)) -%>; - -holder.set(value); -return holder.get(); diff --git a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/subschema.ejs b/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/subschema.ejs deleted file mode 100644 index d1b1d2878d..0000000000 --- a/packages/angular_devkit/core/src/json/schema/serializers/templates/javascript/subschema.ejs +++ /dev/null @@ -1,38 +0,0 @@ -<% -const extras = { - exceptions, - options, - path: (path ? path + '/' : '') + name, - strings, - symbols, - templates, -}; - -if (!schema) { - %>null<% -} else if (schema === true) { -%><%= - templates.prop_any(Object.assign({ - name, - required, - schema: {}, - }, extras)) -%><% -} else if (!('type' in schema)) { -%><%= - templates.prop_any(Object.assign({ - name, - required, - schema: {}, - }, extras)) -%><% -} else { -%><%= - templates['prop_' + schema.type](Object.assign({ - name: name, - required, - schema: schema, - }, extras)) -%><% -} -%> diff --git a/packages/angular_devkit/schematics/package.json b/packages/angular_devkit/schematics/package.json index 542eb45824..a2ffb6ee96 100644 --- a/packages/angular_devkit/schematics/package.json +++ b/packages/angular_devkit/schematics/package.json @@ -17,8 +17,7 @@ ], "dependencies": { "@angular-devkit/core": "0.0.0", - "@ngtools/json-schema": "^1.1.0", - "ajv": "~5.5.1" + "@ngtools/json-schema": "^1.1.0" }, "peerDependencies": { "rxjs": "^5.5.2" diff --git a/packages/angular_devkit/schematics/testing/schematic-test-runner.ts b/packages/angular_devkit/schematics/testing/schematic-test-runner.ts index c8a059b7f8..163e6edadd 100644 --- a/packages/angular_devkit/schematics/testing/schematic-test-runner.ts +++ b/packages/angular_devkit/schematics/testing/schematic-test-runner.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 { logging } from '@angular-devkit/core'; +import { logging, schema } from '@angular-devkit/core'; import { Collection, DelegateTree, @@ -17,7 +17,6 @@ import { VirtualTree, } from '@angular-devkit/schematics'; import { - AjvSchemaRegistry, NodeModulesTestEngineHost, validateOptionsWithSchema, } from '@angular-devkit/schematics/tools'; @@ -44,7 +43,8 @@ export class SchematicTestRunner { this._engineHost.registerCollection(_collectionName, collectionPath); this._logger = new logging.Logger('test'); - this._engineHost.registerOptionsTransform(validateOptionsWithSchema(new AjvSchemaRegistry())); + this._engineHost.registerOptionsTransform( + validateOptionsWithSchema(new schema.CoreSchemaRegistry())); this._collection = this._engine.createCollection(this._collectionName); } diff --git a/packages/angular_devkit/schematics/tools/ajv-option-transform.ts b/packages/angular_devkit/schematics/tools/ajv-option-transform.ts deleted file mode 100644 index 398fdc741b..0000000000 --- a/packages/angular_devkit/schematics/tools/ajv-option-transform.ts +++ /dev/null @@ -1,278 +0,0 @@ -/** - * @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 { JsonArray, JsonObject } from '@angular-devkit/core'; -import * as ajv from 'ajv'; -import * as http from 'http'; -import { Observable } from 'rxjs/Observable'; -import { fromPromise } from 'rxjs/observable/fromPromise'; -import { map } from 'rxjs/operators/map'; -import { - OptionsSchemaRegistry, - OptionsSchemaValidator, - OptionsSchemaValidatorResult, -} from './schema-option-transform'; - - -function _parseJsonPointer(pointer: string): string[] { - if (pointer === '') { return []; } - if (pointer.charAt(0) !== '/') { throw new Error('Invalid JSON pointer: ' + pointer); } - - return pointer.substring(1).split(/\//).map(str => str.replace(/~1/g, '/').replace(/~0/g, '~')); -} - - -interface JsonVisitor { - ( - current: JsonObject | JsonArray, - pointer: string, - parentSchema?: JsonObject | JsonArray, - index?: string, - ): void; -} - - -function _visitJsonSchema(schema: JsonObject, visitor: JsonVisitor) { - const keywords = { - additionalItems: true, - items: true, - contains: true, - additionalProperties: true, - propertyNames: true, - not: true, - }; - - const propsKeywords = { - definitions: true, - properties: true, - patternProperties: true, - dependencies: true, - }; - - function _traverse( - schema: JsonObject | JsonArray, - jsonPtr: string, - rootSchema: JsonObject, - parentSchema?: JsonObject | JsonArray, - keyIndex?: string, - ) { - if (schema && typeof schema == 'object' && !Array.isArray(schema)) { - visitor(schema, jsonPtr, parentSchema, keyIndex); - - for (const key of Object.keys(schema)) { - const sch = schema[key]; - if (Array.isArray(sch)) { - if (key == 'items') { - for (let i = 0; i < sch.length; i++) { - _traverse( - sch[i] as JsonArray, - jsonPtr + '/' + key + '/' + i, - rootSchema, - schema, - '' + i, - ); - } - } - } else if (key in propsKeywords) { - if (sch && typeof sch == 'object') { - for (const prop of Object.keys(sch)) { - _traverse( - sch[prop] as JsonObject, - jsonPtr + '/' + key + '/' + prop.replace(/~/g, '~0').replace(/\//g, '~1'), - rootSchema, - schema, - prop, - ); - } - } - } else if (key in keywords) { - _traverse(sch as JsonObject, jsonPtr + '/' + key, rootSchema, schema, key); - } - } - } - } - - _traverse(schema, '', schema); -} - - -export class AjvSchemaRegistry implements OptionsSchemaRegistry { - private _ajv: ajv.Ajv; - private _uriCache = new Map(); - - constructor() { - /** - * Build an AJV instance that will be used to validate schemas. - */ - this._ajv = ajv({ - removeAdditional: 'all', - useDefaults: true, - loadSchema: (uri: string) => this._fetch(uri) as ajv.Thenable, - }); - - this._ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); - } - - private _clean( - data: any, // tslint:disable-line:no-any - schema: JsonObject, - validate: ajv.ValidateFunction, - parentDataCache: WeakMap, // tslint:disable-line:no-any - ) { - _visitJsonSchema( - schema, - (currentSchema: object, pointer: string, parentSchema?: object, index?: string) => { - // If we're at the root, skip. - if (parentSchema === undefined || index === undefined) { - return; - } - - const parsedPointer = _parseJsonPointer(pointer); - // Every other path fragment is either 'properties', 'items', 'allOf', ... - const nonPropertyParsedPP = parsedPointer.filter((_, i) => !(i % 2)); - // Skip if it's part of a definitions or too complex for us to analyze. - if (nonPropertyParsedPP.some(f => f == 'definitions' || f == 'allOf' || f == 'anyOf')) { - return; - } - - let maybeParentData = parentDataCache.get(parentSchema); - if (!maybeParentData) { - // Every other path fragment is either 'properties' or 'items' in this model. - const parentDataPointer = parsedPointer.filter((_, i) => i % 2); - - // Find the parentData from the list. - maybeParentData = data; - for (const index of parentDataPointer.slice(0, -1)) { - if (maybeParentData[index] === undefined) { - // tslint:disable-next-line:no-any - if (parentSchema.hasOwnProperty('items') || (parentSchema as any)['type'] == 'array') { - maybeParentData[index] = []; - } else { - maybeParentData[index] = {}; - } - } - maybeParentData = maybeParentData[index]; - } - parentDataCache.set(parentSchema, maybeParentData); - } - - if (currentSchema.hasOwnProperty('$ref')) { - const $ref = (currentSchema as { $ref: string })['$ref']; - const refHash = $ref.split('#', 2)[1]; - const refUrl = $ref.startsWith('#') ? $ref : $ref.split('#', 1); - - let refVal = validate; - if (!$ref.startsWith('#')) { - // tslint:disable-next-line:no-any - refVal = (validate.refVal as any)[(validate.refs as any)[refUrl[0]]]; - } - if (refHash) { - // tslint:disable-next-line:no-any - refVal = (refVal.refVal as any)[(refVal.refs as any)['#' + refHash]]; - } - - maybeParentData[index] = {}; - this._clean(maybeParentData[index], refVal.schema as JsonObject, refVal, parentDataCache); - - return; - } else if (!maybeParentData.hasOwnProperty(index)) { - maybeParentData[index] = undefined; - } - }); - } - - private _fetch(uri: string): Promise { - const maybeSchema = this._uriCache.get(uri); - - if (maybeSchema) { - return Promise.resolve(maybeSchema); - } - - return new Promise((resolve, reject) => { - http.get(uri, res => { - if (!res.statusCode || res.statusCode >= 300) { - // Consume the rest of the data to free memory. - res.resume(); - reject(`Request failed. Status Code: ${res.statusCode}`); - } else { - res.setEncoding('utf8'); - let data = ''; - res.on('data', chunk => { - data += chunk; - }); - res.on('end', () => { - try { - const json = JSON.parse(data); - this._uriCache.set(uri, json); - resolve(json); - } catch (err) { - reject(err); - } - }); - } - }); - }); - } - - compile(schema: Object): Observable { - // Supports both synchronous and asynchronous compilation, by trying the synchronous - // version first, then if refs are missing this will fails. - // We also add any refs from external fetched schemas so that those will also be used - // in synchronous (if available). - let validator: Observable; - try { - const maybeFnValidate = this._ajv.compile(schema); - validator = Observable.of(maybeFnValidate); - } catch (e) { - // Propagate the error. - if (!(e instanceof (ajv.MissingRefError as {} as Function))) { - throw e; - } - - validator = new Observable(obs => { - this._ajv.compileAsync(schema) - .then(validate => { - obs.next(validate); - obs.complete(); - }, err => { - obs.error(err); - }); - }); - } - - return validator - .pipe( - // tslint:disable-next-line:no-any - map(validate => (data: any): Observable => { - const result = validate(data); - const resultObs = typeof result == 'boolean' - ? Observable.of(result) - : fromPromise(result as PromiseLike); - - return resultObs - .pipe( - map(result => { - if (result) { - // tslint:disable-next-line:no-any - const schemaDataMap = new WeakMap(); - schemaDataMap.set(schema, data); - - this._clean(data, schema as JsonObject, validate, schemaDataMap); - - return { success: true } as OptionsSchemaValidatorResult; - } - - return { - success: false, - errors: (validate.errors || []).map((err: ajv.ErrorObject) => err.message), - } as OptionsSchemaValidatorResult; - }), - ); - }), - ); - } -} diff --git a/packages/angular_devkit/schematics/tools/index.ts b/packages/angular_devkit/schematics/tools/index.ts index d2fac31bff..4ce36d5ec2 100644 --- a/packages/angular_devkit/schematics/tools/index.ts +++ b/packages/angular_devkit/schematics/tools/index.ts @@ -14,5 +14,4 @@ export { FileSystemEngineHost } from './file-system-engine-host'; export { NodeModulesEngineHost } from './node-module-engine-host'; export { NodeModulesTestEngineHost } from './node-modules-test-engine-host'; -export { AjvSchemaRegistry } from './ajv-option-transform'; export { validateOptionsWithSchema } from './schema-option-transform'; diff --git a/packages/angular_devkit/schematics/tools/schema-option-transform.ts b/packages/angular_devkit/schematics/tools/schema-option-transform.ts index d3136c484a..b142ae208d 100644 --- a/packages/angular_devkit/schematics/tools/schema-option-transform.ts +++ b/packages/angular_devkit/schematics/tools/schema-option-transform.ts @@ -5,7 +5,10 @@ * 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 { BaseException } from '@angular-devkit/core'; +import { + BaseException, + schema, +} from '@angular-devkit/core'; import { SchematicDescription } from '@angular-devkit/schematics'; import { Observable } from 'rxjs/Observable'; import { first } from 'rxjs/operators/first'; @@ -26,20 +29,6 @@ export class InvalidInputOptions extends BaseException { } -export interface OptionsSchemaValidatorResult { - success: boolean; - errors?: string[]; -} - -export interface OptionsSchemaValidator { - // tslint:disable-next-line:no-any - (data: any): Observable; -} - -export interface OptionsSchemaRegistry { - compile(schema: Object): Observable; -} - // tslint:disable-next-line:no-any function _deepCopy(object: T): T { const copy = {} as T; @@ -57,7 +46,7 @@ function _deepCopy(object: T): T { // This can only be used in NodeJS. -export function validateOptionsWithSchema(registry: OptionsSchemaRegistry) { +export function validateOptionsWithSchema(registry: schema.SchemaRegistry) { return (schematic: SchematicDesc, options: T): Observable => { // Prevent a schematic from changing the options object by making a copy of it. options = _deepCopy(options); diff --git a/packages/angular_devkit/schematics_cli/bin/schematics.ts b/packages/angular_devkit/schematics_cli/bin/schematics.ts index b1f5b632fe..9a6d4273ac 100644 --- a/packages/angular_devkit/schematics_cli/bin/schematics.ts +++ b/packages/angular_devkit/schematics_cli/bin/schematics.ts @@ -6,7 +6,11 @@ * 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, tags, terminal } from '@angular-devkit/core'; +import { + schema, + tags, + terminal, +} from '@angular-devkit/core'; import { createConsoleLogger } from '@angular-devkit/core/node'; import { DryRunEvent, @@ -17,7 +21,6 @@ import { Tree, } from '@angular-devkit/schematics'; import { - AjvSchemaRegistry, FileSystemHost, NodeModulesEngineHost, validateOptionsWithSchema, @@ -125,7 +128,7 @@ const engine = new SchematicEngine(engineHost); // Add support for schemaJson. -engineHost.registerOptionsTransform(validateOptionsWithSchema(new AjvSchemaRegistry())); +engineHost.registerOptionsTransform(validateOptionsWithSchema(new schema.CoreSchemaRegistry())); /** @@ -260,17 +263,10 @@ schematic.call(args, host, { debug, logger: logger.asApi() }) }) .subscribe({ error(err: Error) { - // Add extra processing to output better error messages. - if (err instanceof schema.javascript.RequiredValueMissingException) { - logger.fatal('Missing argument on the command line: ' + err.path.split('/').pop()); - } else if (err instanceof schema.javascript.InvalidPropertyNameException) { - logger.fatal('A non-supported argument was passed: ' + err.path.split('/').pop()); + if (debug) { + logger.fatal('An error occured:\n' + err.stack); } else { - if (debug) { - logger.fatal('An error occured:\n' + err.stack); - } else { - logger.fatal(err.message); - } + logger.fatal(err.message); } process.exit(1); }, From 3e87640e9ab9aa9c7e7c567046073d7c32e3fafa Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Tue, 19 Dec 2017 14:20:29 +0000 Subject: [PATCH 04/10] docs(@angular-devkit/core): add JSON schema headers --- .monorepo.json | 6 ++++ README.md | 2 +- packages/angular_devkit/core/README.md | 42 ++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 packages/angular_devkit/core/README.md diff --git a/.monorepo.json b/.monorepo.json index 2df50a1c95..ea559dee2c 100644 --- a/.monorepo.json +++ b/.monorepo.json @@ -58,6 +58,12 @@ }, "@angular-devkit/core": { "name": "Core", + "links": [ + { + "label": "README", + "url": "https://github.com/angular/devkit/blob/master/packages/angular_devkit/core/README.md" + } + ], "version": "0.0.23", "hash": "45c3c8b7d60b038bbedd3000ebca1b88" }, diff --git a/README.md b/README.md index 27c429464f..8fa9b28628 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ This is a monorepo which contains many packages: | Project | Package | Version | Links | |---|---|---|---| **Build Optimizer** | [`@angular-devkit/build-optimizer`](http://npmjs.com/packages/@angular-devkit/build-optimizer) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fbuild-optimizer/latest.svg)](http://npmjs.com/packages/@angular-devkit/build-optimizer) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/build_optimizer/README.md) -**Core** | [`@angular-devkit/core`](http://npmjs.com/packages/@angular-devkit/core) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fcore/latest.svg)](http://npmjs.com/packages/@angular-devkit/core) | +**Core** | [`@angular-devkit/core`](http://npmjs.com/packages/@angular-devkit/core) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fcore/latest.svg)](http://npmjs.com/packages/@angular-devkit/core) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/core/README.md) **Schematics** | [`@angular-devkit/schematics`](http://npmjs.com/packages/@angular-devkit/schematics) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fschematics/latest.svg)](http://npmjs.com/packages/@angular-devkit/schematics) | [![README](https://img.shields.io/badge/README--green.svg)](https://github.com/angular/devkit/blob/master/packages/angular_devkit/schematics/README.md) **Schematics CLI** | [`@angular-devkit/schematics-cli`](http://npmjs.com/packages/@angular-devkit/schematics-cli) | [![latest](https://img.shields.io/npm/v/%40angular-devkit%2Fschematics-cli/latest.svg)](http://npmjs.com/packages/@angular-devkit/schematics-cli) | diff --git a/packages/angular_devkit/core/README.md b/packages/angular_devkit/core/README.md new file mode 100644 index 0000000000..fe8ca4c306 --- /dev/null +++ b/packages/angular_devkit/core/README.md @@ -0,0 +1,42 @@ +# Core +> Shared utilities for Angular DevKit. + +# Exception + +# Json + +## Schema + +### SchemaValidatorResult +``` +export interface SchemaValidatorResult { + success: boolean; + errors?: string[]; +} +``` + +### SchemaValidator + +``` +export interface SchemaValidator { + (data: any): Observable; +} +``` + +### SchemaRegistry + +``` +export interface SchemaRegistry { + compile(schema: Object): Observable; +} +``` + +### CoreSchemaRegistry + +`SchemaRegistry` implementation using https://github.com/epoberezkin/ajv. + +# Logger + +# Utils + +# Virtual FS \ No newline at end of file From 5a84d6ce5cd0beeb5c8621aa8d609814f8fa6993 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 20 Dec 2017 14:46:48 +0000 Subject: [PATCH 05/10] feat(@angular-devkit/core): support schema custom formats --- packages/angular_devkit/core/README.md | 17 ++++ .../core/src/json/schema/interface.ts | 12 ++- .../core/src/json/schema/registry.ts | 37 +++++++- .../core/src/json/schema/registry_spec.ts | 93 +++++++++++++++++-- 4 files changed, 149 insertions(+), 10 deletions(-) diff --git a/packages/angular_devkit/core/README.md b/packages/angular_devkit/core/README.md index fe8ca4c306..9657238b8c 100644 --- a/packages/angular_devkit/core/README.md +++ b/packages/angular_devkit/core/README.md @@ -23,17 +23,34 @@ export interface SchemaValidator { } ``` +### SchemaFormatter + +``` +export interface SchemaFormatter { + readonly async: boolean; + validate(data: any): boolean | Observable; +} +``` + ### SchemaRegistry ``` export interface SchemaRegistry { compile(schema: Object): Observable; + addFormat(name: string, formatter: SchemaFormatter): void; } ``` ### CoreSchemaRegistry `SchemaRegistry` implementation using https://github.com/epoberezkin/ajv. +Constructor accepts object containing `SchemaFormatter` that will be added automatically. + +``` +export class CoreSchemaRegistry implements SchemaRegistry { + constructor(formats: { [name: string]: SchemaFormatter} = {}) {} +} +``` # Logger diff --git a/packages/angular_devkit/core/src/json/schema/interface.ts b/packages/angular_devkit/core/src/json/schema/interface.ts index a634354046..71322f7136 100644 --- a/packages/angular_devkit/core/src/json/schema/interface.ts +++ b/packages/angular_devkit/core/src/json/schema/interface.ts @@ -13,13 +13,23 @@ export interface SchemaValidatorResult { errors?: string[]; } - export interface SchemaValidator { // tslint:disable-next-line:no-any (data: any): Observable; } +export interface SchemaFormatter { + readonly async: boolean; + // tslint:disable-next-line:no-any + validate(data: any): boolean | Observable; +} + +export interface SchemaFormat { + name: string; + formatter: SchemaFormatter; +} export interface SchemaRegistry { compile(schema: Object): Observable; + addFormat(format: SchemaFormat): void; } diff --git a/packages/angular_devkit/core/src/json/schema/registry.ts b/packages/angular_devkit/core/src/json/schema/registry.ts index 4caeeda6bc..142c9f9285 100644 --- a/packages/angular_devkit/core/src/json/schema/registry.ts +++ b/packages/angular_devkit/core/src/json/schema/registry.ts @@ -12,7 +12,13 @@ import 'rxjs/add/observable/of'; import { fromPromise } from 'rxjs/observable/fromPromise'; import { map } from 'rxjs/operators/map'; import { JsonArray, JsonObject } from '../interface'; -import { SchemaRegistry, SchemaValidator, SchemaValidatorResult } from './interface'; +import { + SchemaFormat, + SchemaFormatter, + SchemaRegistry, + SchemaValidator, + SchemaValidatorResult, +} from './interface'; function _parseJsonPointer(pointer: string): string[] { @@ -101,13 +107,21 @@ export class CoreSchemaRegistry implements SchemaRegistry { private _ajv: ajv.Ajv; private _uriCache = new Map(); - constructor() { + constructor(formats: SchemaFormat[] = []) { /** * Build an AJV instance that will be used to validate schemas. */ + + const formatsObj: { [name: string]: SchemaFormatter } = {}; + + for (const format of formats) { + formatsObj[format.name] = format.formatter; + } + this._ajv = ajv({ removeAdditional: 'all', useDefaults: true, + formats: formatsObj, loadSchema: (uri: string) => this._fetch(uri) as ajv.Thenable, }); @@ -265,11 +279,28 @@ export class CoreSchemaRegistry implements SchemaRegistry { return { success: false, - errors: (validate.errors || []).map((err: ajv.ErrorObject) => err.message), + errors: (validate.errors || []) + .map((err: ajv.ErrorObject) => `${err.dataPath} ${err.message}`), } as SchemaValidatorResult; }), ); }), ); } + + addFormat(format: SchemaFormat): void { + // tslint:disable-next-line:no-any + const validate = (data: any) => { + const result = format.formatter.validate(data); + + return result instanceof Observable ? result.toPromise() : result; + }; + + this._ajv.addFormat(format.name, { + async: format.formatter.async, + validate, + // AJV typings list `compare` as required, but it is optional. + // tslint:disable-next-line:no-any + } as any); + } } diff --git a/packages/angular_devkit/core/src/json/schema/registry_spec.ts b/packages/angular_devkit/core/src/json/schema/registry_spec.ts index 917cf6bde8..ece9121815 100644 --- a/packages/angular_devkit/core/src/json/schema/registry_spec.ts +++ b/packages/angular_devkit/core/src/json/schema/registry_spec.ts @@ -5,15 +5,15 @@ * 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 */ -// tslint:disable:no-any +import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/mergeMap'; import { CoreSchemaRegistry } from './registry'; describe('CoreSchemaRegistry', () => { it('works asynchronously', done => { - const registry = new CoreSchemaRegistry(); - const data: any = {}; // tslint:disable:no-any + const registry = new CoreSchemaRegistry(); + const data: any = {}; // tslint:disable-line:no-any registry .compile({ @@ -38,14 +38,14 @@ describe('CoreSchemaRegistry', () => { expect(data.tslint).not.toBeUndefined(); }) .subscribe(done, done.fail); - }); + }); // Synchronous failure is only used internally. // If it's meant to be used externally then this test should change to truly be synchronous // (i.e. not relyign on the observable). it('works synchronously', done => { - const registry = new CoreSchemaRegistry(); - const data: any = {}; // tslint:disable:no-any + const registry = new CoreSchemaRegistry(); + const data: any = {}; // tslint:disable-line:no-any let isDone = false; registry @@ -73,4 +73,85 @@ describe('CoreSchemaRegistry', () => { expect(isDone).toBe(true); done(); }); + + it('supports sync format', done => { + const registry = new CoreSchemaRegistry(); + const data = { str: 'hotdog' }; + const format = { + name: 'is-hotdog', + formatter: { + async: false, + validate: (str: string) => str === 'hotdog', + }, + }; + + registry.addFormat(format); + + registry + .compile({ + properties: { + str: { type: 'string', format: 'is-hotdog' }, + }, + }) + .mergeMap(validator => validator(data)) + .map(result => { + expect(result.success).toBe(true); + }) + .subscribe(done, done.fail); + }); + + it('supports async format', done => { + const registry = new CoreSchemaRegistry(); + const data = { str: 'hotdog' }; + const format = { + name: 'is-hotdog', + formatter: { + async: true, + validate: (str: string) => Observable.of(str === 'hotdog'), + }, + }; + + registry.addFormat(format); + + registry + .compile({ + $async: true, + properties: { + str: { type: 'string', format: 'is-hotdog' }, + }, + }) + .mergeMap(validator => validator(data)) + .map(result => { + expect(result.success).toBe(true); + }) + .subscribe(done, done.fail); + }); + + it('shows dataPath and message on error', done => { + const registry = new CoreSchemaRegistry(); + const data = { hotdot: 'hotdog', banana: 'banana' }; + const format = { + name: 'is-hotdog', + formatter: { + async: false, + validate: (str: string) => str === 'hotdog', + }, + }; + + registry.addFormat(format); + + registry + .compile({ + properties: { + hotdot: { type: 'string', format: 'is-hotdog' }, + banana: { type: 'string', format: 'is-hotdog' }, + }, + }) + .mergeMap(validator => validator(data)) + .map(result => { + expect(result.success).toBe(false); + expect(result.errors && result.errors[0]).toBe('.banana should match format "is-hotdog"'); + }) + .subscribe(done, done.fail); + }); }); From e8a9cb6d9dd2c5a8c2424ea170dfbd6441907772 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Fri, 22 Dec 2017 13:34:37 +0000 Subject: [PATCH 06/10] feat(@angular-devkit/schematics): add custom schema formats --- .../schematics/src/formats/app-name.ts | 22 ++++++ .../schematics/src/formats/app-name_spec.ts | 69 +++++++++++++++++++ .../src/formats/format-validator.ts | 28 ++++++++ .../schematics/src/formats/html-selector.ts | 22 ++++++ .../src/formats/html-selector_spec.ts | 57 +++++++++++++++ .../schematics/src/formats/index.ts | 11 +++ .../schematics/src/formats/path.ts | 24 +++++++ .../schematics/src/formats/path_spec.ts | 35 ++++++++++ .../angular_devkit/schematics/src/index.ts | 2 + .../testing/schematic-test-runner.ts | 11 ++- .../schematics_cli/bin/schematics.ts | 9 ++- 11 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 packages/angular_devkit/schematics/src/formats/app-name.ts create mode 100644 packages/angular_devkit/schematics/src/formats/app-name_spec.ts create mode 100644 packages/angular_devkit/schematics/src/formats/format-validator.ts create mode 100644 packages/angular_devkit/schematics/src/formats/html-selector.ts create mode 100644 packages/angular_devkit/schematics/src/formats/html-selector_spec.ts create mode 100644 packages/angular_devkit/schematics/src/formats/index.ts create mode 100644 packages/angular_devkit/schematics/src/formats/path.ts create mode 100644 packages/angular_devkit/schematics/src/formats/path_spec.ts diff --git a/packages/angular_devkit/schematics/src/formats/app-name.ts b/packages/angular_devkit/schematics/src/formats/app-name.ts new file mode 100644 index 0000000000..1d1761858d --- /dev/null +++ b/packages/angular_devkit/schematics/src/formats/app-name.ts @@ -0,0 +1,22 @@ +/** + * @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 { htmlSelectorRe } from './html-selector'; + + +const unsupportedProjectNames = ['test', 'ember', 'ember-cli', 'vendor', 'app']; + +export const appNameFormat: schema.SchemaFormat = { + name: 'app-name', + formatter: { + async: false, + validate: (appName: string) => htmlSelectorRe.test(appName) + && unsupportedProjectNames.indexOf(appName) === -1, + }, +}; diff --git a/packages/angular_devkit/schematics/src/formats/app-name_spec.ts b/packages/angular_devkit/schematics/src/formats/app-name_spec.ts new file mode 100644 index 0000000000..6098f92c7e --- /dev/null +++ b/packages/angular_devkit/schematics/src/formats/app-name_spec.ts @@ -0,0 +1,69 @@ +/** + * @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 { appNameFormat } from './app-name'; +import { formatValidator } from './format-validator'; + + +describe('Schematics app name format', () => { + it('accepts correct app name', done => { + const data = { appName: 'my-app' }; + const dataSchema = { + properties: { appName: { type: 'string', format: 'app-name' } }, + }; + + formatValidator(data, dataSchema, [appNameFormat]) + .map(result => expect(result.success).toBe(true)) + .subscribe(done, done.fail); + }); + + it('rejects app name starting with invalid characters', done => { + const data = { appName: 'my-app$' }; + const dataSchema = { + properties: { appName: { type: 'string', format: 'app-name' } }, + }; + + formatValidator(data, dataSchema, [appNameFormat]) + .map(result => expect(result.success).toBe(false)) + .subscribe(done, done.fail); + }); + + it('rejects app name starting with number', done => { + const data = { appName: '1app' }; + const dataSchema = { + properties: { appName: { type: 'string', format: 'app-name' } }, + }; + + formatValidator(data, dataSchema, [appNameFormat]) + .map(result => expect(result.success).toBe(false)) + .subscribe(done, done.fail); + }); + + it('rejects unsupported app names', done => { + const data = { + appName1: 'test', + appName2: 'ember', + appName3: 'ember-cli', + appName4: 'vendor', + appName5: 'app', + }; + const dataSchema = { + properties: { + appName1: { type: 'string', format: 'app-name' }, + appName2: { type: 'string', format: 'app-name' }, + appName3: { type: 'string', format: 'app-name' }, + appName4: { type: 'string', format: 'app-name' }, + appName5: { type: 'string', format: 'app-name' }, + }, + }; + + formatValidator(data, dataSchema, [appNameFormat]) + .map(result => expect(result.success).toBe(false)) + .subscribe(done, done.fail); + }); +}); diff --git a/packages/angular_devkit/schematics/src/formats/format-validator.ts b/packages/angular_devkit/schematics/src/formats/format-validator.ts new file mode 100644 index 0000000000..8acfc7c871 --- /dev/null +++ b/packages/angular_devkit/schematics/src/formats/format-validator.ts @@ -0,0 +1,28 @@ +/** + * @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 { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/mergeMap'; + + +export function formatValidator( + data: Object, + dataSchema: Object, + formats: schema.SchemaFormat[], +): Observable { + const registry = new schema.CoreSchemaRegistry(); + + for (const format of formats) { + registry.addFormat(format); + } + + return registry + .compile(dataSchema) + .mergeMap(validator => validator(data)); +} diff --git a/packages/angular_devkit/schematics/src/formats/html-selector.ts b/packages/angular_devkit/schematics/src/formats/html-selector.ts new file mode 100644 index 0000000000..6d7d9ada9b --- /dev/null +++ b/packages/angular_devkit/schematics/src/formats/html-selector.ts @@ -0,0 +1,22 @@ +/** + * @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'; + + +// Must start with a letter, and must contain only alphanumeric characters or dashes. +// When adding a dash the segment after the dash must also start with a letter. +export const htmlSelectorRe = /^[a-zA-Z][.0-9a-zA-Z]*(:?-[a-zA-Z][.0-9a-zA-Z]*)*$/; + +export const htmlSelectorFormat: schema.SchemaFormat = { + name: 'html-selector', + formatter: { + async: false, + validate: (selector: string) => htmlSelectorRe.test(selector), + }, +}; diff --git a/packages/angular_devkit/schematics/src/formats/html-selector_spec.ts b/packages/angular_devkit/schematics/src/formats/html-selector_spec.ts new file mode 100644 index 0000000000..c2b264de4c --- /dev/null +++ b/packages/angular_devkit/schematics/src/formats/html-selector_spec.ts @@ -0,0 +1,57 @@ +/** + * @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 { formatValidator } from './format-validator'; +import { htmlSelectorFormat } from './html-selector'; + + +describe('Schematics HTML selector format', () => { + it('accepts correct selectors', done => { + const data = { selector: 'my-selector' }; + const dataSchema = { + properties: { selector: { type: 'string', format: 'html-selector' } }, + }; + + formatValidator(data, dataSchema, [htmlSelectorFormat]) + .map(result => expect(result.success).toBe(true)) + .subscribe(done, done.fail); + }); + + it('rejects selectors starting with invalid characters', done => { + const data = { selector: 'my-selector$' }; + const dataSchema = { + properties: { selector: { type: 'string', format: 'html-selector' } }, + }; + + formatValidator(data, dataSchema, [htmlSelectorFormat]) + .map(result => expect(result.success).toBe(false)) + .subscribe(done, done.fail); + }); + + it('rejects selectors starting with number', done => { + const data = { selector: '1selector' }; + const dataSchema = { + properties: { selector: { type: 'string', format: 'html-selector' } }, + }; + + formatValidator(data, dataSchema, [htmlSelectorFormat]) + .map(result => expect(result.success).toBe(false)) + .subscribe(done, done.fail); + }); + + it('rejects selectors with non-letter after dash', done => { + const data = { selector: 'my-1selector' }; + const dataSchema = { + properties: { selector: { type: 'string', format: 'html-selector' } }, + }; + + formatValidator(data, dataSchema, [htmlSelectorFormat]) + .map(result => expect(result.success).toBe(false)) + .subscribe(done, done.fail); + }); +}); diff --git a/packages/angular_devkit/schematics/src/formats/index.ts b/packages/angular_devkit/schematics/src/formats/index.ts new file mode 100644 index 0000000000..aa8033eefb --- /dev/null +++ b/packages/angular_devkit/schematics/src/formats/index.ts @@ -0,0 +1,11 @@ +/** + * @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 + */ + +export * from './app-name'; +export * from './html-selector'; +export * from './path'; diff --git a/packages/angular_devkit/schematics/src/formats/path.ts b/packages/angular_devkit/schematics/src/formats/path.ts new file mode 100644 index 0000000000..0c854957f1 --- /dev/null +++ b/packages/angular_devkit/schematics/src/formats/path.ts @@ -0,0 +1,24 @@ +/** + * @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 { normalize, schema } from '@angular-devkit/core'; + + +export const pathFormat: schema.SchemaFormat = { + name: 'path', + formatter: { + async: false, + validate: (path: string) => { + // Check path is normalized already. + return path === normalize(path); + // TODO: check if path is valid (is that just checking if it's normalized?) + // TODO: check path is from root of schematics even if passed absolute + // TODO: error out if path is outside of host + }, + }, +}; diff --git a/packages/angular_devkit/schematics/src/formats/path_spec.ts b/packages/angular_devkit/schematics/src/formats/path_spec.ts new file mode 100644 index 0000000000..64b7ab8330 --- /dev/null +++ b/packages/angular_devkit/schematics/src/formats/path_spec.ts @@ -0,0 +1,35 @@ +/** + * @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 { formatValidator } from './format-validator'; +import { pathFormat } from './path'; + + +describe('Schematics Path format', () => { + it('accepts correct Paths', done => { + const data = { path: 'a/b/c' }; + const dataSchema = { + properties: { path: { type: 'string', format: 'path' } }, + }; + + formatValidator(data, dataSchema, [pathFormat]) + .map(result => expect(result.success).toBe(true)) + .subscribe(done, done.fail); + }); + + it('rejects Paths that are not normalized', done => { + const data = { path: 'a/b/c/../' }; + const dataSchema = { + properties: { path: { type: 'string', format: 'path' } }, + }; + + formatValidator(data, dataSchema, [pathFormat]) + .map(result => expect(result.success).toBe(false)) + .subscribe(done, done.fail); + }); +}); diff --git a/packages/angular_devkit/schematics/src/index.ts b/packages/angular_devkit/schematics/src/index.ts index 60c9cc706f..5a711ad20b 100644 --- a/packages/angular_devkit/schematics/src/index.ts +++ b/packages/angular_devkit/schematics/src/index.ts @@ -33,6 +33,8 @@ export {UpdateRecorder} from './tree/interface'; export * from './engine/schematic'; export * from './sink/dryrun'; export {FileSystemSink} from './sink/filesystem'; +import * as formats from './formats'; +export { formats }; export interface TreeConstructor { diff --git a/packages/angular_devkit/schematics/testing/schematic-test-runner.ts b/packages/angular_devkit/schematics/testing/schematic-test-runner.ts index 163e6edadd..b09b603513 100644 --- a/packages/angular_devkit/schematics/testing/schematic-test-runner.ts +++ b/packages/angular_devkit/schematics/testing/schematic-test-runner.ts @@ -15,6 +15,7 @@ import { SchematicEngine, Tree, VirtualTree, + formats, } from '@angular-devkit/schematics'; import { NodeModulesTestEngineHost, @@ -43,8 +44,14 @@ export class SchematicTestRunner { this._engineHost.registerCollection(_collectionName, collectionPath); this._logger = new logging.Logger('test'); - this._engineHost.registerOptionsTransform( - validateOptionsWithSchema(new schema.CoreSchemaRegistry())); + const schemaFormats = [ + formats.appNameFormat, + formats.htmlSelectorFormat, + formats.pathFormat, + ]; + const registry = new schema.CoreSchemaRegistry(schemaFormats); + + this._engineHost.registerOptionsTransform(validateOptionsWithSchema(registry)); this._collection = this._engine.createCollection(this._collectionName); } diff --git a/packages/angular_devkit/schematics_cli/bin/schematics.ts b/packages/angular_devkit/schematics_cli/bin/schematics.ts index 9a6d4273ac..bd18e14581 100644 --- a/packages/angular_devkit/schematics_cli/bin/schematics.ts +++ b/packages/angular_devkit/schematics_cli/bin/schematics.ts @@ -19,6 +19,7 @@ import { FileSystemTree, SchematicEngine, Tree, + formats, } from '@angular-devkit/schematics'; import { FileSystemHost, @@ -128,7 +129,13 @@ const engine = new SchematicEngine(engineHost); // Add support for schemaJson. -engineHost.registerOptionsTransform(validateOptionsWithSchema(new schema.CoreSchemaRegistry())); +const schemaFormats = [ + formats.appNameFormat, + formats.htmlSelectorFormat, + formats.pathFormat, +]; +const registry = new schema.CoreSchemaRegistry(schemaFormats); +engineHost.registerOptionsTransform(validateOptionsWithSchema(registry)); /** From ad46db8357d0c81662607faef285a69a43797560 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Fri, 29 Dec 2017 15:06:53 +0000 Subject: [PATCH 07/10] refactor(@schematics/angular): use schema format --- .../schematics/angular/app-shell/index_spec.ts | 3 +-- .../schematics/angular/app-shell/schema.json | 15 ++++++++++++++- .../schematics/angular/application/index_spec.ts | 1 - .../schematics/angular/application/schema.json | 7 ++++++- packages/schematics/angular/class/schema.json | 3 +++ .../schematics/angular/component/index_spec.ts | 16 ++-------------- .../schematics/angular/component/schema.json | 6 +++++- .../schematics/angular/directive/schema.json | 5 +++++ packages/schematics/angular/enum/schema.json | 3 +++ packages/schematics/angular/guard/schema.json | 6 ++++-- .../schematics/angular/interface/schema.json | 3 +++ packages/schematics/angular/module/schema.json | 3 +++ packages/schematics/angular/pipe/schema.json | 6 ++++-- packages/schematics/angular/service/schema.json | 6 ++++-- .../schematics/angular/universal/index_spec.ts | 7 +++---- .../schematics/angular/universal/schema.json | 12 +++++++++++- 16 files changed, 71 insertions(+), 31 deletions(-) diff --git a/packages/schematics/angular/app-shell/index_spec.ts b/packages/schematics/angular/app-shell/index_spec.ts index 800787a563..fa69b5d55c 100644 --- a/packages/schematics/angular/app-shell/index_spec.ts +++ b/packages/schematics/angular/app-shell/index_spec.ts @@ -25,8 +25,7 @@ describe('App Shell Schematic', () => { let appTree: Tree; const appOptions: ApplicationOptions = { directory: '', - name: 'app', - prefix: '', + name: 'appshell-app', sourceDir: 'src', inlineStyle: false, inlineTemplate: false, diff --git a/packages/schematics/angular/app-shell/schema.json b/packages/schematics/angular/app-shell/schema.json index d066efdc10..e4b2a8cbf6 100644 --- a/packages/schematics/angular/app-shell/schema.json +++ b/packages/schematics/angular/app-shell/schema.json @@ -22,64 +22,77 @@ "name": { "type": "string", + "format": "app-name", "description": "Name of the universal app" }, "appId": { "type": "string", + "format": "app-name", "description": "The appId to use withServerTransition.", "default": "serverApp" }, "outDir": { "type": "string", + "format": "path", "description": "The output directory for build results.", - "default": "dist-server/" + "default": "dist-server" }, "root": { "type": "string", + "format": "path", "description": "The root directory of the app.", "default": "src" }, "index": { "type": "string", + "format": "path", "description": "Name of the index file", "default": "index.html" }, "main": { "type": "string", + "format": "path", "description": "The name of the main entry-point file.", "default": "main.server.ts" }, "test": { "type": "string", + "format": "path", "description": "The name of the test entry-point file." }, "tsconfigFileName": { "type": "string", + "format": "path", "default": "tsconfig.server", "description": "The name of the TypeScript configuration file." }, "testTsconfigFileName": { "type": "string", + "format": "path", "description": "The name of the TypeScript configuration file for tests.", "default": "tsconfig.spec" }, "appDir": { "type": "string", + "format": "path", "description": "The name of the application directory.", "default": "app" }, "rootModuleFileName": { "type": "string", + "format": "path", "description": "The name of the root module file", "default": "app.server.module.ts" }, "rootModuleClassName": { "type": "string", + "format": "app-name", "description": "The name of the root module class.", "default": "AppServerModule" }, "sourceDir": { "type": "string", + "format": "path", "description": "The path of the source directory.", "default": "src", "alias": "sd" diff --git a/packages/schematics/angular/application/index_spec.ts b/packages/schematics/angular/application/index_spec.ts index af02ff53a4..f57ce5668d 100644 --- a/packages/schematics/angular/application/index_spec.ts +++ b/packages/schematics/angular/application/index_spec.ts @@ -19,7 +19,6 @@ describe('Application Schematic', () => { const defaultOptions: ApplicationOptions = { directory: 'foo', name: 'foo', - prefix: '', sourceDir: 'src', inlineStyle: false, inlineTemplate: false, diff --git a/packages/schematics/angular/application/schema.json b/packages/schematics/angular/application/schema.json index 4e6e6c890c..f288630af7 100644 --- a/packages/schematics/angular/application/schema.json +++ b/packages/schematics/angular/application/schema.json @@ -6,17 +6,20 @@ "properties": { "directory": { "type": "string", + "format": "path", "description": "The directory name to create the app in.", "alias": "dir" }, "path": { "type": "string", + "format": "path", "description": "The path of the application.", "default": "app", "visible": false }, "sourceDir": { "type": "string", + "format": "path", "description": "The path of the source directory.", "default": "src", "alias": "sd", @@ -24,7 +27,8 @@ }, "name": { "description": "The name of the application.", - "type": "string" + "type": "string", + "format": "app-name" }, "inlineStyle": { "description": "Specifies if the style will be in the ts file.", @@ -55,6 +59,7 @@ }, "prefix": { "type": "string", + "format": "html-selector", "description": "The prefix to apply to generated selectors.", "default": "app", "alias": "p" diff --git a/packages/schematics/angular/class/schema.json b/packages/schematics/angular/class/schema.json index a746f7569a..1cc74bf93c 100644 --- a/packages/schematics/angular/class/schema.json +++ b/packages/schematics/angular/class/schema.json @@ -10,18 +10,21 @@ }, "path": { "type": "string", + "format": "path", "description": "The path to create the class.", "default": "app", "visible": false }, "sourceDir": { "type": "string", + "format": "path", "description": "The path of the source directory.", "default": "src", "visible": false }, "appRoot": { "type": "string", + "format": "path", "description": "The root of the application.", "visible": false }, diff --git a/packages/schematics/angular/component/index_spec.ts b/packages/schematics/angular/component/index_spec.ts index 6badbcbbbb..8e0b65d7bb 100644 --- a/packages/schematics/angular/component/index_spec.ts +++ b/packages/schematics/angular/component/index_spec.ts @@ -28,7 +28,7 @@ describe('Component Schematic', () => { spec: true, module: undefined, export: false, - prefix: undefined, + prefix: 'app', }; let appTree: Tree; @@ -175,18 +175,6 @@ describe('Component Schematic', () => { expect(files.indexOf(`${root}.ts`)).toBeGreaterThanOrEqual(0); }); - it('should handle ".." in a path', () => { - const options = { ...defaultOptions, path: 'app/a/../c' }; - - const tree = schematicRunner.runSchematic('component', options, appTree); - const files = tree.files; - const root = `/src/app/c/foo/foo.component`; - expect(files.indexOf(`${root}.css`)).toBeGreaterThanOrEqual(0); - expect(files.indexOf(`${root}.html`)).toBeGreaterThanOrEqual(0); - expect(files.indexOf(`${root}.spec.ts`)).toBeGreaterThanOrEqual(0); - expect(files.indexOf(`${root}.ts`)).toBeGreaterThanOrEqual(0); - }); - it('should use the prefix', () => { const options = { ...defaultOptions, prefix: 'pre' }; @@ -196,7 +184,7 @@ describe('Component Schematic', () => { }); it('should not use a prefix if none is passed', () => { - const options = { ...defaultOptions, prefix: '' }; + const options = { ...defaultOptions, prefix: undefined }; const tree = schematicRunner.runSchematic('component', options, appTree); const content = getFileContent(tree, '/src/app/foo/foo.component.ts'); diff --git a/packages/schematics/angular/component/schema.json b/packages/schematics/angular/component/schema.json index 990a30665a..7479112b38 100644 --- a/packages/schematics/angular/component/schema.json +++ b/packages/schematics/angular/component/schema.json @@ -6,12 +6,14 @@ "properties": { "path": { "type": "string", + "format": "path", "description": "The path to create the component.", "default": "app", "visible": false }, "sourceDir": { "type": "string", + "format": "path", "description": "The path of the source directory.", "default": "src", "alias": "sd", @@ -19,6 +21,7 @@ }, "appRoot": { "type": "string", + "format": "path", "description": "The root of the application.", "visible": false }, @@ -53,8 +56,8 @@ }, "prefix": { "type": "string", + "format": "html-selector", "description": "The prefix to apply to generated selectors.", - "default": "app", "alias": "p" }, "styleext": { @@ -79,6 +82,7 @@ }, "selector": { "type": "string", + "format": "html-selector", "description": "The selector to use for the component." }, "module": { diff --git a/packages/schematics/angular/directive/schema.json b/packages/schematics/angular/directive/schema.json index eb4e3e4c5d..ce8de2a228 100644 --- a/packages/schematics/angular/directive/schema.json +++ b/packages/schematics/angular/directive/schema.json @@ -10,23 +10,27 @@ }, "path": { "type": "string", + "format": "path", "description": "The path to create the directive.", "default": "app", "visible": false }, "appRoot": { "type": "string", + "format": "path", "description": "The root of the application.", "visible": false }, "prefix": { "type": "string", + "format": "html-selector", "description": "The prefix to apply to generated selectors.", "default": "app", "alias": "p" }, "sourceDir": { "type": "string", + "format": "path", "description": "The path of the source directory.", "default": "src", "visible": false @@ -43,6 +47,7 @@ }, "selector": { "type": "string", + "format": "html-selector", "description": "The selector to use for the directive." }, "flat": { diff --git a/packages/schematics/angular/enum/schema.json b/packages/schematics/angular/enum/schema.json index dc5dc5ccf3..c30c95ee5d 100644 --- a/packages/schematics/angular/enum/schema.json +++ b/packages/schematics/angular/enum/schema.json @@ -10,18 +10,21 @@ }, "path": { "type": "string", + "format": "path", "description": "The path to create the enum.", "default": "app", "visible": false }, "sourceDir": { "type": "string", + "format": "path", "description": "The path of the source directory.", "default": "src", "visible": false }, "appRoot": { "type": "string", + "format": "path", "description": "The root of the application.", "visible": false } diff --git a/packages/schematics/angular/guard/schema.json b/packages/schematics/angular/guard/schema.json index 614e286da3..14b3a441fc 100644 --- a/packages/schematics/angular/guard/schema.json +++ b/packages/schematics/angular/guard/schema.json @@ -21,23 +21,25 @@ "module": { "type": "string", "description": "Allows specification of the declaring module.", - "alias": "m", - "subtype": "filepath" + "alias": "m" }, "path": { "type": "string", + "format": "path", "description": "The path to create the guard.", "default": "app", "visible": false }, "sourceDir": { "type": "string", + "format": "path", "description": "The path of the source directory.", "default": "src", "visible": false }, "appRoot": { "type": "string", + "format": "path", "description": "The root of the application.", "visible": false } diff --git a/packages/schematics/angular/interface/schema.json b/packages/schematics/angular/interface/schema.json index 62765b2820..3403e12d23 100644 --- a/packages/schematics/angular/interface/schema.json +++ b/packages/schematics/angular/interface/schema.json @@ -10,18 +10,21 @@ }, "path": { "type": "string", + "format": "path", "description": "The path to create the interface.", "default": "app", "visible": false }, "sourceDir": { "type": "string", + "format": "path", "description": "The path of the source directory.", "default": "src", "visible": false }, "appRoot": { "type": "string", + "format": "path", "description": "The root of the application.", "visible": false }, diff --git a/packages/schematics/angular/module/schema.json b/packages/schematics/angular/module/schema.json index 5dc35b6224..c124c2184f 100644 --- a/packages/schematics/angular/module/schema.json +++ b/packages/schematics/angular/module/schema.json @@ -10,18 +10,21 @@ }, "path": { "type": "string", + "format": "path", "description": "The path to create the module.", "default": "app", "visible": false }, "sourceDir": { "type": "string", + "format": "path", "description": "The path of the source directory.", "default": "src", "visible": false }, "appRoot": { "type": "string", + "format": "path", "description": "The root of the application.", "visible": false }, diff --git a/packages/schematics/angular/pipe/schema.json b/packages/schematics/angular/pipe/schema.json index 505eef4a25..009fe827cf 100644 --- a/packages/schematics/angular/pipe/schema.json +++ b/packages/schematics/angular/pipe/schema.json @@ -10,18 +10,21 @@ }, "path": { "type": "string", + "format": "path", "description": "The path to create the pipe.", "default": "app", "visible": false }, "sourceDir": { "type": "string", + "format": "path", "description": "The path of the source directory.", "default": "src", "visible": false }, "appRoot": { "type": "string", + "format": "path", "description": "The root of the application.", "visible": false }, @@ -44,8 +47,7 @@ "type": "string", "default": "", "description": "Allows specification of the declaring module.", - "alias": "m", - "subtype": "filepath" + "alias": "m" }, "export": { "type": "boolean", diff --git a/packages/schematics/angular/service/schema.json b/packages/schematics/angular/service/schema.json index 082ef3c672..400f3939d8 100644 --- a/packages/schematics/angular/service/schema.json +++ b/packages/schematics/angular/service/schema.json @@ -10,18 +10,21 @@ }, "path": { "type": "string", + "format": "path", "description": "The path to create the service.", "default": "app", "visible": false }, "sourceDir": { "type": "string", + "format": "path", "description": "The path of the source directory.", "default": "src", "visible": false }, "appRoot": { "type": "string", + "format": "path", "description": "The root of the application.", "visible": false }, @@ -39,8 +42,7 @@ "type": "string", "default": "", "description": "Allows specification of the providing module.", - "alias": "m", - "subtype": "filepath" + "alias": "m" } }, "required": [ diff --git a/packages/schematics/angular/universal/index_spec.ts b/packages/schematics/angular/universal/index_spec.ts index 07b7a705d4..d591e53cb1 100644 --- a/packages/schematics/angular/universal/index_spec.ts +++ b/packages/schematics/angular/universal/index_spec.ts @@ -26,8 +26,7 @@ describe('Universal Schematic', () => { beforeEach(() => { const appOptions: ApplicationOptions = { directory: '', - name: 'app', - prefix: '', + name: 'universal-app', sourceDir: 'src', inlineStyle: false, inlineTemplate: false, @@ -63,7 +62,7 @@ describe('Universal Schematic', () => { const file = tree.files.filter(f => f === filePath)[0]; expect(file).toBeDefined(); const contents = tree.read(filePath); - expect(contents).toMatch(/\"outDir\": \"\.\.\/dist-server\/\"/); + expect(contents).toMatch(/\"outDir\": \"\.\.\/dist-server\"/); }); it('should add dependency: @angular/platform-server', () => { @@ -83,7 +82,7 @@ describe('Universal Schematic', () => { const app = config.apps[1]; expect(app.platform).toEqual('server'); expect(app.root).toEqual('src'); - expect(app.outDir).toEqual('dist-server/'); + expect(app.outDir).toEqual('dist-server'); expect(app.index).toEqual('index.html'); expect(app.main).toEqual('main.server.ts'); expect(app.test).toEqual('test.ts'); diff --git a/packages/schematics/angular/universal/schema.json b/packages/schematics/angular/universal/schema.json index d6ce90f041..ad8d3cd4c5 100644 --- a/packages/schematics/angular/universal/schema.json +++ b/packages/schematics/angular/universal/schema.json @@ -15,31 +15,37 @@ }, "appId": { "type": "string", + "format": "app-name", "description": "The appId to use withServerTransition.", "default": "serverApp" }, "outDir": { "type": "string", + "format": "path", "description": "The output directory for build results.", - "default": "dist-server/" + "default": "dist-server" }, "root": { "type": "string", + "format": "path", "description": "The root directory of the app.", "default": "src" }, "index": { "type": "string", + "format": "path", "description": "Name of the index file", "default": "index.html" }, "main": { "type": "string", + "format": "path", "description": "The name of the main entry-point file.", "default": "main.server.ts" }, "test": { "type": "string", + "format": "path", "description": "The name of the test entry-point file." }, "tsconfigFileName": { @@ -49,16 +55,19 @@ }, "testTsconfigFileName": { "type": "string", + "format": "path", "description": "The name of the TypeScript configuration file for tests.", "default": "tsconfig.spec" }, "appDir": { "type": "string", + "format": "path", "description": "The name of the application directory.", "default": "app" }, "rootModuleFileName": { "type": "string", + "format": "path", "description": "The name of the root module file", "default": "app.server.module.ts" }, @@ -69,6 +78,7 @@ }, "sourceDir": { "type": "string", + "format": "path", "description": "The path of the source directory.", "default": "src", "alias": "sd" From e5480e6318325ec5487ce255e4276664b546211d Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Tue, 2 Jan 2018 10:41:01 +0000 Subject: [PATCH 08/10] refactor(@angular-devkit/schematics): add standardFormats array --- .../schematics/src/formats/index.ts | 16 +++++++++++++--- .../schematics_cli/bin/schematics.ts | 7 +------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/angular_devkit/schematics/src/formats/index.ts b/packages/angular_devkit/schematics/src/formats/index.ts index aa8033eefb..fdd30b789e 100644 --- a/packages/angular_devkit/schematics/src/formats/index.ts +++ b/packages/angular_devkit/schematics/src/formats/index.ts @@ -6,6 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -export * from './app-name'; -export * from './html-selector'; -export * from './path'; +import { schema } from '@angular-devkit/core'; +import { appNameFormat } from './app-name'; +export { appNameFormat } from './app-name'; +import { htmlSelectorFormat } from './html-selector'; +export { htmlSelectorFormat } from './html-selector'; +import { pathFormat } from './path'; +export { pathFormat } from './path'; + +export const standardFormats: schema.SchemaFormat[] = [ + appNameFormat, + htmlSelectorFormat, + pathFormat, +]; diff --git a/packages/angular_devkit/schematics_cli/bin/schematics.ts b/packages/angular_devkit/schematics_cli/bin/schematics.ts index bd18e14581..e251a43eb1 100644 --- a/packages/angular_devkit/schematics_cli/bin/schematics.ts +++ b/packages/angular_devkit/schematics_cli/bin/schematics.ts @@ -129,12 +129,7 @@ const engine = new SchematicEngine(engineHost); // Add support for schemaJson. -const schemaFormats = [ - formats.appNameFormat, - formats.htmlSelectorFormat, - formats.pathFormat, -]; -const registry = new schema.CoreSchemaRegistry(schemaFormats); +const registry = new schema.CoreSchemaRegistry(formats.standardFormats); engineHost.registerOptionsTransform(validateOptionsWithSchema(registry)); From c5f9fbd4046f3d0475ec378f5461a7d3c49a9e4c Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Tue, 2 Jan 2018 10:45:56 +0000 Subject: [PATCH 09/10] docs(@angular-devkit/schematics): document formats --- packages/angular_devkit/schematics/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/angular_devkit/schematics/README.md b/packages/angular_devkit/schematics/README.md index 28ca53d1eb..e4d543b8f9 100644 --- a/packages/angular_devkit/schematics/README.md +++ b/packages/angular_devkit/schematics/README.md @@ -27,6 +27,7 @@ The tooling is responsible for the following tasks: 1. Create the Schematic Engine, and pass in a Collection and Schematic loader. 1. Understand and respect the Schematics metadata and dependencies between collections. Schematics can refer to dependencies, and it's the responsibility of the tool to honor those dependencies. The reference CLI uses NPM packages for its collections. 1. Create the Options object. Options can be anything, but the schematics can specify a JSON Schema that should be respected. The reference CLI, for example, parse the arguments as a JSON object and validate it with the Schema specified by the collection. + 1. Schematics provides some JSON Schema formats for validation that tooling should add. These validate paths, html selectors and app names. Please check the reference CLI for how these can be added. 1. Call the schematics with the original Tree. The tree should represent the initial state of the filesystem. The reference CLI uses the current directory for this. 1. Create a Sink and commit the result of the schematics to the Sink. Many sinks are provided by the library; FileSystemSink and DryRunSink are examples. 1. Output any logs propagated by the library, including debugging information. From 586d7322c0de09af748cea8ab1961ab391f6c1f0 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 10 Jan 2018 20:40:24 +0000 Subject: [PATCH 10/10] feat(@angular-devkit/schematics): allow any project name --- .../schematics/src/formats/app-name.ts | 22 ------ .../schematics/src/formats/app-name_spec.ts | 69 ------------------- .../schematics/src/formats/index.ts | 3 - .../testing/schematic-test-runner.ts | 7 +- .../schematics/angular/app-shell/schema.json | 6 +- .../angular/application/schema.json | 2 +- .../schematics/angular/universal/schema.json | 2 +- 7 files changed, 6 insertions(+), 105 deletions(-) delete mode 100644 packages/angular_devkit/schematics/src/formats/app-name.ts delete mode 100644 packages/angular_devkit/schematics/src/formats/app-name_spec.ts diff --git a/packages/angular_devkit/schematics/src/formats/app-name.ts b/packages/angular_devkit/schematics/src/formats/app-name.ts deleted file mode 100644 index 1d1761858d..0000000000 --- a/packages/angular_devkit/schematics/src/formats/app-name.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @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 { htmlSelectorRe } from './html-selector'; - - -const unsupportedProjectNames = ['test', 'ember', 'ember-cli', 'vendor', 'app']; - -export const appNameFormat: schema.SchemaFormat = { - name: 'app-name', - formatter: { - async: false, - validate: (appName: string) => htmlSelectorRe.test(appName) - && unsupportedProjectNames.indexOf(appName) === -1, - }, -}; diff --git a/packages/angular_devkit/schematics/src/formats/app-name_spec.ts b/packages/angular_devkit/schematics/src/formats/app-name_spec.ts deleted file mode 100644 index 6098f92c7e..0000000000 --- a/packages/angular_devkit/schematics/src/formats/app-name_spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @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 { appNameFormat } from './app-name'; -import { formatValidator } from './format-validator'; - - -describe('Schematics app name format', () => { - it('accepts correct app name', done => { - const data = { appName: 'my-app' }; - const dataSchema = { - properties: { appName: { type: 'string', format: 'app-name' } }, - }; - - formatValidator(data, dataSchema, [appNameFormat]) - .map(result => expect(result.success).toBe(true)) - .subscribe(done, done.fail); - }); - - it('rejects app name starting with invalid characters', done => { - const data = { appName: 'my-app$' }; - const dataSchema = { - properties: { appName: { type: 'string', format: 'app-name' } }, - }; - - formatValidator(data, dataSchema, [appNameFormat]) - .map(result => expect(result.success).toBe(false)) - .subscribe(done, done.fail); - }); - - it('rejects app name starting with number', done => { - const data = { appName: '1app' }; - const dataSchema = { - properties: { appName: { type: 'string', format: 'app-name' } }, - }; - - formatValidator(data, dataSchema, [appNameFormat]) - .map(result => expect(result.success).toBe(false)) - .subscribe(done, done.fail); - }); - - it('rejects unsupported app names', done => { - const data = { - appName1: 'test', - appName2: 'ember', - appName3: 'ember-cli', - appName4: 'vendor', - appName5: 'app', - }; - const dataSchema = { - properties: { - appName1: { type: 'string', format: 'app-name' }, - appName2: { type: 'string', format: 'app-name' }, - appName3: { type: 'string', format: 'app-name' }, - appName4: { type: 'string', format: 'app-name' }, - appName5: { type: 'string', format: 'app-name' }, - }, - }; - - formatValidator(data, dataSchema, [appNameFormat]) - .map(result => expect(result.success).toBe(false)) - .subscribe(done, done.fail); - }); -}); diff --git a/packages/angular_devkit/schematics/src/formats/index.ts b/packages/angular_devkit/schematics/src/formats/index.ts index fdd30b789e..2cfab19db6 100644 --- a/packages/angular_devkit/schematics/src/formats/index.ts +++ b/packages/angular_devkit/schematics/src/formats/index.ts @@ -7,15 +7,12 @@ */ import { schema } from '@angular-devkit/core'; -import { appNameFormat } from './app-name'; -export { appNameFormat } from './app-name'; import { htmlSelectorFormat } from './html-selector'; export { htmlSelectorFormat } from './html-selector'; import { pathFormat } from './path'; export { pathFormat } from './path'; export const standardFormats: schema.SchemaFormat[] = [ - appNameFormat, htmlSelectorFormat, pathFormat, ]; diff --git a/packages/angular_devkit/schematics/testing/schematic-test-runner.ts b/packages/angular_devkit/schematics/testing/schematic-test-runner.ts index b09b603513..6a5354bf8d 100644 --- a/packages/angular_devkit/schematics/testing/schematic-test-runner.ts +++ b/packages/angular_devkit/schematics/testing/schematic-test-runner.ts @@ -44,12 +44,7 @@ export class SchematicTestRunner { this._engineHost.registerCollection(_collectionName, collectionPath); this._logger = new logging.Logger('test'); - const schemaFormats = [ - formats.appNameFormat, - formats.htmlSelectorFormat, - formats.pathFormat, - ]; - const registry = new schema.CoreSchemaRegistry(schemaFormats); + const registry = new schema.CoreSchemaRegistry(formats.standardFormats); this._engineHost.registerOptionsTransform(validateOptionsWithSchema(registry)); this._collection = this._engine.createCollection(this._collectionName); diff --git a/packages/schematics/angular/app-shell/schema.json b/packages/schematics/angular/app-shell/schema.json index e4b2a8cbf6..cfb3c8d37b 100644 --- a/packages/schematics/angular/app-shell/schema.json +++ b/packages/schematics/angular/app-shell/schema.json @@ -22,12 +22,12 @@ "name": { "type": "string", - "format": "app-name", + "format": "html-selector", "description": "Name of the universal app" }, "appId": { "type": "string", - "format": "app-name", + "format": "html-selector", "description": "The appId to use withServerTransition.", "default": "serverApp" }, @@ -86,7 +86,7 @@ }, "rootModuleClassName": { "type": "string", - "format": "app-name", + "format": "html-selector", "description": "The name of the root module class.", "default": "AppServerModule" }, diff --git a/packages/schematics/angular/application/schema.json b/packages/schematics/angular/application/schema.json index f288630af7..b2239a5e78 100644 --- a/packages/schematics/angular/application/schema.json +++ b/packages/schematics/angular/application/schema.json @@ -28,7 +28,7 @@ "name": { "description": "The name of the application.", "type": "string", - "format": "app-name" + "format": "html-selector" }, "inlineStyle": { "description": "Specifies if the style will be in the ts file.", diff --git a/packages/schematics/angular/universal/schema.json b/packages/schematics/angular/universal/schema.json index ad8d3cd4c5..fa8f3528a8 100644 --- a/packages/schematics/angular/universal/schema.json +++ b/packages/schematics/angular/universal/schema.json @@ -15,7 +15,7 @@ }, "appId": { "type": "string", - "format": "app-name", + "format": "html-selector", "description": "The appId to use withServerTransition.", "default": "serverApp" },