From 74f7cddf9e0a5af013ccd15350f018849846daf5 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 10 Jan 2017 16:19:42 -0800 Subject: [PATCH] feat(@ngtools/json-schema): Introduce a separate package for JSON schema. (#3927) This is the same code we used before, but: 1. its in a separate package, 2. it also support new serialization to .d.ts, 3. OneOf is now supported entirely (instead of the previous hack). Also, removed the schema.d.ts file and generate it before building the package. It is not git ignored and changing it will not work (it will be overwritten). --- package.json | 1 - packages/@ngtools/json-schema/package.json | 32 ++ packages/@ngtools/json-schema/src/error.ts | 12 + packages/@ngtools/json-schema/src/index.ts | 1 + .../@ngtools/json-schema/src/mimetypes.ts | 34 +++ packages/@ngtools/json-schema/src/node.ts | 42 +++ .../json-schema/src}/schema-class-factory.ts | 10 +- .../json-schema/src/schema-tree.spec.ts | 34 +++ .../json-schema/src}/schema-tree.ts | 274 ++++++++++++------ .../@ngtools/json-schema/src/serializer.ts | 26 ++ .../json-schema/src/serializers/dts.spec.ts | 30 ++ .../json-schema/src/serializers/dts.ts | 150 ++++++++++ .../json-schema/src/serializers/json.spec.ts | 32 ++ .../json-schema/src/serializers/json.ts} | 111 +++---- .../@ngtools/json-schema/tests/schema1.json | 84 ++++++ .../@ngtools/json-schema/tests/value1-1.json | 8 + .../@ngtools/json-schema/tests/value1.json | 7 + packages/@ngtools/json-schema/tsconfig.json | 27 ++ packages/angular-cli/lib/config/.gitignore | 1 + packages/angular-cli/lib/config/schema.d.ts | 79 ----- packages/angular-cli/lib/config/schema.json | 3 +- .../angular-cli/models/config}/config.spec.ts | 62 ++-- packages/angular-cli/models/config/config.ts | 2 +- .../models/config}/spec-schema.d.ts | 0 .../models/config}/spec-schema.json | 7 +- packages/angular-cli/package.json | 1 + packages/angular-cli/tsconfig.json | 1 + scripts/build-schema-dts.js | 28 ++ scripts/publish/build.js | 7 + 29 files changed, 844 insertions(+), 262 deletions(-) create mode 100644 packages/@ngtools/json-schema/package.json create mode 100644 packages/@ngtools/json-schema/src/error.ts create mode 100644 packages/@ngtools/json-schema/src/index.ts create mode 100644 packages/@ngtools/json-schema/src/mimetypes.ts create mode 100644 packages/@ngtools/json-schema/src/node.ts rename packages/{angular-cli/models/json-schema => @ngtools/json-schema/src}/schema-class-factory.ts (96%) create mode 100644 packages/@ngtools/json-schema/src/schema-tree.spec.ts rename packages/{angular-cli/models/json-schema => @ngtools/json-schema/src}/schema-tree.ts (53%) create mode 100644 packages/@ngtools/json-schema/src/serializer.ts create mode 100644 packages/@ngtools/json-schema/src/serializers/dts.spec.ts create mode 100644 packages/@ngtools/json-schema/src/serializers/dts.ts create mode 100644 packages/@ngtools/json-schema/src/serializers/json.spec.ts rename packages/{angular-cli/models/json-schema/serializer.ts => @ngtools/json-schema/src/serializers/json.ts} (50%) create mode 100644 packages/@ngtools/json-schema/tests/schema1.json create mode 100644 packages/@ngtools/json-schema/tests/value1-1.json create mode 100644 packages/@ngtools/json-schema/tests/value1.json create mode 100644 packages/@ngtools/json-schema/tsconfig.json create mode 100644 packages/angular-cli/lib/config/.gitignore delete mode 100644 packages/angular-cli/lib/config/schema.d.ts rename {tests/models => packages/angular-cli/models/config}/config.spec.ts (50%) rename {tests/models => packages/angular-cli/models/config}/spec-schema.d.ts (100%) rename {tests/models => packages/angular-cli/models/config}/spec-schema.json (91%) create mode 100644 scripts/build-schema-dts.js diff --git a/package.json b/package.json index 493e12c07820..3c481bf3af3c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "test:deps": "node scripts/publish/validate_dependencies.js", "test:inspect": "node --inspect --debug-brk tests/runner", "test:packages": "node scripts/run-packages-spec.js", - "build-config-interface": "dtsgen packages/angular-cli/lib/config/schema.json --out packages/angular-cli/lib/config/schema.d.ts", "eslint": "eslint .", "tslint": "tslint \"**/*.ts\" -c tslint.json -e \"**/blueprints/*/files/**/*.ts\" -e \"node_modules/**\" -e \"tmp/**\" -e \"dist/**\"", "lint": "npm-run-all -c eslint tslint" diff --git a/packages/@ngtools/json-schema/package.json b/packages/@ngtools/json-schema/package.json new file mode 100644 index 000000000000..a1bf952ca658 --- /dev/null +++ b/packages/@ngtools/json-schema/package.json @@ -0,0 +1,32 @@ +{ + "name": "@ngtools/json-schema", + "version": "1.2.1", + "description": "Schema validating and reading for configurations, similar to Angular CLI config.", + "main": "./src/index.js", + "typings": "src/index.d.ts", + "license": "MIT", + "keywords": [ + "angular", + "json", + "json-schema", + "schema", + "config" + ], + "repository": { + "type": "git", + "url": "https://github.com/angular/angular-cli.git" + }, + "author": "angular", + "bugs": { + "url": "https://github.com/angular/angular-cli/issues" + }, + "homepage": "https://github.com/angular/angular-cli/tree/master/packages/@ngtools/json-schema", + "engines": { + "node": ">= 4.1.0", + "npm": ">= 3.0.0" + }, + "dependencies": { + }, + "peerDependencies": { + } +} diff --git a/packages/@ngtools/json-schema/src/error.ts b/packages/@ngtools/json-schema/src/error.ts new file mode 100644 index 000000000000..5863a5decd4e --- /dev/null +++ b/packages/@ngtools/json-schema/src/error.ts @@ -0,0 +1,12 @@ + +export class JsonSchemaErrorBase extends Error { + constructor(message?: string) { + super(); + + if (message) { + this.message = message; + } else { + this.message = (this.constructor).name; + } + } +} diff --git a/packages/@ngtools/json-schema/src/index.ts b/packages/@ngtools/json-schema/src/index.ts new file mode 100644 index 000000000000..1021cae0c409 --- /dev/null +++ b/packages/@ngtools/json-schema/src/index.ts @@ -0,0 +1 @@ +export {SchemaClass, SchemaClassFactory} from './schema-class-factory'; diff --git a/packages/@ngtools/json-schema/src/mimetypes.ts b/packages/@ngtools/json-schema/src/mimetypes.ts new file mode 100644 index 000000000000..6c22ccb9fa81 --- /dev/null +++ b/packages/@ngtools/json-schema/src/mimetypes.ts @@ -0,0 +1,34 @@ +import {JsonSchemaErrorBase} from './error'; +import {Serializer, WriterFn} from './serializer'; +import {JsonSerializer} from './serializers/json'; +import {DTsSerializer} from './serializers/dts'; + + +export class UnknownMimetype extends JsonSchemaErrorBase {} + + +export function createSerializerFromMimetype(mimetype: string, + writer: WriterFn, + ...opts: any[]): Serializer { + let Klass: { new (writer: WriterFn, ...args: any[]): Serializer } = null; + switch (mimetype) { + case 'application/json': Klass = JsonSerializer; break; + case 'text/json': Klass = JsonSerializer; break; + case 'text/x.typescript': Klass = DTsSerializer; break; + case 'text/x.dts': Klass = DTsSerializer; break; + + default: throw new UnknownMimetype(); + } + + return new Klass(writer, ...opts); + +} + + +declare module './serializer' { + namespace Serializer { + export let fromMimetype: typeof createSerializerFromMimetype; + } +} + +Serializer.fromMimetype = createSerializerFromMimetype; diff --git a/packages/@ngtools/json-schema/src/node.ts b/packages/@ngtools/json-schema/src/node.ts new file mode 100644 index 000000000000..873ac6697503 --- /dev/null +++ b/packages/@ngtools/json-schema/src/node.ts @@ -0,0 +1,42 @@ +import {Serializer} from './serializer'; + + +// A TypeScript Type. This can be used to do `new tsType(value)`. +// `null` implies any type; be careful. +export type TypeScriptType = typeof Number + | typeof Boolean + | typeof String + | typeof Object + | typeof Array + | null; + + +// The most generic interface for a schema node. This is used by the serializers. +export interface SchemaNode { + readonly name: string; + readonly type: string; + readonly tsType: TypeScriptType; + readonly defined: boolean; + readonly dirty: boolean; + readonly frozen: boolean; + readonly readOnly: boolean; + readonly defaultValue: any | null; + readonly required: boolean; + readonly parent: SchemaNode | null; + + // Schema related properties. + readonly description: string | null; + + // Object-only properties. `null` for everything else. + readonly children: { [key: string]: SchemaNode } | null; + + // Array-only properties. `null` for everything else. + readonly items: SchemaNode[] | null; + readonly itemPrototype: SchemaNode | null; + + // Mutable properties. + value: any; + + // Serialization. + serialize(serializer: Serializer): void; +} diff --git a/packages/angular-cli/models/json-schema/schema-class-factory.ts b/packages/@ngtools/json-schema/src/schema-class-factory.ts similarity index 96% rename from packages/angular-cli/models/json-schema/schema-class-factory.ts rename to packages/@ngtools/json-schema/src/schema-class-factory.ts index 9af3b537031d..35d4e98a6611 100644 --- a/packages/angular-cli/models/json-schema/schema-class-factory.ts +++ b/packages/@ngtools/json-schema/src/schema-class-factory.ts @@ -1,8 +1,11 @@ -import {NgToolkitError} from '../error'; import {Serializer} from './serializer'; import {RootSchemaTreeNode, SchemaTreeNode} from './schema-tree'; +import {JsonSchemaErrorBase} from './error'; -export class InvalidJsonPath extends NgToolkitError {} +import './mimetypes'; + + +export class InvalidJsonPath extends JsonSchemaErrorBase {} // The schema tree node property of the SchemaClass. @@ -66,6 +69,9 @@ export interface SchemaClass extends Object { $$defined(path: string): boolean; $$delete(path: string): void; + // Direct access to the schema. + $$schema(): RootSchemaTreeNode; + $$serialize(mimetype?: string): string; } diff --git a/packages/@ngtools/json-schema/src/schema-tree.spec.ts b/packages/@ngtools/json-schema/src/schema-tree.spec.ts new file mode 100644 index 000000000000..46fee6f9815b --- /dev/null +++ b/packages/@ngtools/json-schema/src/schema-tree.spec.ts @@ -0,0 +1,34 @@ +import {readFileSync} from 'fs'; +import {join} from 'path'; + +import {RootSchemaTreeNode} from './schema-tree'; + + +describe('SchemaTreeNode', () => { + +}); + + +describe('OneOfSchemaTreeNode', () => { + const schemaJsonFilePath = join(__dirname, '../tests/schema1.json'); + const schemaJson = JSON.parse(readFileSync(schemaJsonFilePath, 'utf-8')); + const valueJsonFilePath = join(__dirname, '../tests/value1-1.json'); + const valueJson = JSON.parse(readFileSync(valueJsonFilePath, 'utf-8')); + + + it('works', () => { + const proto: any = Object.create(null); + new RootSchemaTreeNode(proto, { + value: valueJson, + schema: schemaJson + }); + + expect(proto.oneOfKey2 instanceof Array).toBe(true); + expect(proto.oneOfKey2.length).toBe(2); + + // Set it to a string, which is valid. + proto.oneOfKey2 = 'hello'; + expect(proto.oneOfKey2 instanceof Array).toBe(false); + }); +}); + diff --git a/packages/angular-cli/models/json-schema/schema-tree.ts b/packages/@ngtools/json-schema/src/schema-tree.ts similarity index 53% rename from packages/angular-cli/models/json-schema/schema-tree.ts rename to packages/@ngtools/json-schema/src/schema-tree.ts index 2b80057b6424..75d1fe2fe814 100644 --- a/packages/angular-cli/models/json-schema/schema-tree.ts +++ b/packages/@ngtools/json-schema/src/schema-tree.ts @@ -1,11 +1,11 @@ -import {NgToolkitError} from '../error'; - +import {JsonSchemaErrorBase} from './error'; import {Serializer} from './serializer'; +import {SchemaNode, TypeScriptType} from './node'; -export class InvalidSchema extends NgToolkitError {} -export class MissingImplementationError extends NgToolkitError {} -export class SettingReadOnlyPropertyError extends NgToolkitError {} +export class InvalidSchema extends JsonSchemaErrorBase {} +export class MissingImplementationError extends JsonSchemaErrorBase {} +export class SettingReadOnlyPropertyError extends JsonSchemaErrorBase {} export interface Schema { @@ -18,7 +18,7 @@ export type TreeNodeConstructorArgument = { parent?: SchemaTreeNode; name?: string; value: T; - forward: SchemaTreeNode; + forward?: SchemaTreeNode; schema: Schema; }; @@ -26,7 +26,7 @@ export type TreeNodeConstructorArgument = { /** * Holds all the information, including the value, of a node in the schema tree. */ -export abstract class SchemaTreeNode { +export abstract class SchemaTreeNode implements SchemaNode { // Hierarchy objects protected _parent: SchemaTreeNode; @@ -51,7 +51,9 @@ export abstract class SchemaTreeNode { this._schema = null; this._value = null; - this._forward.dispose(); + if (this._forward) { + this._forward.dispose(); + } this._forward = null; } @@ -67,22 +69,42 @@ export abstract class SchemaTreeNode { } } + get value(): T { return this.get(); } + abstract get type(): string; + abstract get tsType(): TypeScriptType; abstract destroy(): void; + abstract get defaultValue(): any | null; get name() { return this._name; } get readOnly(): boolean { return this._schema['readOnly']; } + get frozen(): boolean { return true; } + get description() { + return 'description' in this._schema ? this._schema['description'] : null; + } + get required() { + if (!this._parent) { + return false; + } + return this._parent.isChildRequired(this.name); + } + + isChildRequired(name: string) { return false; } + get parent(): SchemaTreeNode { return this._parent; } - get children(): { [key: string]: SchemaTreeNode} { return null; } + get children(): { [key: string]: SchemaTreeNode } | null { return null; } + get items(): SchemaTreeNode[] | null { return null; } + get itemPrototype(): SchemaTreeNode | null { return null; } abstract get(): T; - set(v: T) { + set(v: T, force = false) { if (!this.readOnly) { throw new MissingImplementationError(); } throw new SettingReadOnlyPropertyError(); }; + isCompatible(v: any) { return false; } - abstract serialize(serializer: Serializer, value?: T): void; + abstract serialize(serializer: Serializer): void; protected static _defineProperty(proto: any, treeNode: SchemaTreeNode): void { if (treeNode.readOnly) { @@ -104,14 +126,15 @@ export abstract class SchemaTreeNode { /** Base Class used for Non-Leaves TreeNode. Meaning they can have children. */ export abstract class NonLeafSchemaTreeNode extends SchemaTreeNode { dispose() { - for (const key of Object.keys(this.children)) { + for (const key of Object.keys(this.children || {})) { this.children[key].dispose(); } + for (let item of this.items || []) { + item.dispose(); + } super.dispose(); } - // Non leaves are read-only. - get readOnly() { return true; } get() { if (this.defined) { return this._value; @@ -129,24 +152,8 @@ export abstract class NonLeafSchemaTreeNode extends SchemaTreeNode { protected _createChildProperty(name: string, value: T, forward: SchemaTreeNode, schema: Schema, define = true): SchemaTreeNode { - let type: string; - - if (!schema['oneOf']) { - type = schema['type']; - } else { - let testValue = value || schema['default']; - // Match existing value to one of the schema types - for (let testSchema of schema['oneOf']) { - if ((testSchema['type'] === 'array' && Array.isArray(testValue)) - || typeof testValue === testSchema['type']) { - type = testSchema['type']; - schema = testSchema; - break; - } - } - } - - let Klass: any = null; + let type: string = schema['oneOf'] ? 'oneOf' : schema['type']; + let Klass: { new (arg: TreeNodeConstructorArgument): SchemaTreeNode } = null; switch (type) { case 'object': Klass = ObjectSchemaTreeNode; break; @@ -156,9 +163,10 @@ export abstract class NonLeafSchemaTreeNode extends SchemaTreeNode { case 'number': Klass = NumberSchemaTreeNode; break; case 'integer': Klass = IntegerSchemaTreeNode; break; + case 'oneOf': Klass = OneOfSchemaTreeNode; break; + default: - console.error('Type ' + type + ' not understood by SchemaClassFactory.'); - return null; + throw new InvalidSchema('Type ' + type + ' not understood by SchemaClassFactory.'); } const metaData = new Klass({ parent: this, forward, value, schema, name }); @@ -170,20 +178,92 @@ export abstract class NonLeafSchemaTreeNode extends SchemaTreeNode { } +export class OneOfSchemaTreeNode extends NonLeafSchemaTreeNode { + protected _typesPrototype: SchemaTreeNode[]; + protected _currentTypeHolder: SchemaTreeNode | null; + + constructor(metaData: TreeNodeConstructorArgument) { + super(metaData); + + let { value, forward, schema } = metaData; + this._typesPrototype = schema['oneOf'].map((schema: Object) => { + return this._createChildProperty('', '', forward, schema, false); + }); + + this._currentTypeHolder = null; + this._set(value, true, false); + } + + _set(v: any, init: boolean, force: boolean) { + if (!init && this.readOnly && !force) { + throw new SettingReadOnlyPropertyError(); + } + + // Find the first type prototype that is compatible with the + let proto: SchemaTreeNode = null; + for (let i = 0; i < this._typesPrototype.length; i++) { + const p = this._typesPrototype[i]; + if (p.isCompatible(v)) { + proto = p; + break; + } + } + if (proto == null) { + return; + } + + if (!init) { + this.dirty = true; + } + + this._currentTypeHolder = proto; + this._currentTypeHolder.set(v, true); + } + + set(v: any, force = false) { + return this._set(v, false, force); + } + + get(): any { + return this._currentTypeHolder ? this._currentTypeHolder.get() : null; + } + get defaultValue(): any | null { + return null; + } + + get defined() { return this._currentTypeHolder ? this._currentTypeHolder.defined : false; } + get items() { return this._typesPrototype; } + get type() { return 'oneOf'; } + get tsType(): null { return null; } + + serialize(serializer: Serializer) { serializer.outputOneOf(this); } +} + + /** A Schema Tree Node that represents an object. */ export class ObjectSchemaTreeNode extends NonLeafSchemaTreeNode<{[key: string]: any}> { // The map of all children metadata. protected _children: { [key: string]: SchemaTreeNode }; + protected _frozen: boolean = false; constructor(metaData: TreeNodeConstructorArgument) { super(metaData); - let { value, forward, schema } = metaData; - if (value) { - this._defined = true; + this._set(metaData.value, true, false); + } + + _set(value: any, init: boolean, force: boolean) { + if (!init && this.readOnly && !force) { + throw new SettingReadOnlyPropertyError(); } + + const schema = this._schema; + const forward = this._forward; + + this._defined = !!value; this._children = Object.create(null); this._value = Object.create(null); + this._dirty = this._dirty || !init; if (schema['properties']) { for (const name of Object.keys(schema['properties'])) { @@ -195,11 +275,14 @@ export class ObjectSchemaTreeNode extends NonLeafSchemaTreeNode<{[key: string]: propertySchema); } } else if (!schema['additionalProperties']) { - throw new InvalidSchema(); + throw new InvalidSchema('Schema does not have a properties, but doesnt allow for ' + + 'additional properties.'); } if (!schema['additionalProperties']) { + this._frozen = true; Object.freeze(this._value); + Object.freeze(this._children); } else if (value) { // Set other properties which don't have a schema. for (const key of Object.keys(value)) { @@ -208,27 +291,28 @@ export class ObjectSchemaTreeNode extends NonLeafSchemaTreeNode<{[key: string]: } } } - - Object.freeze(this._children); } - serialize(serializer: Serializer, value = this._value) { - serializer.object(() => { - for (const key of Object.keys(value)) { - if (this._children[key]) { - if (this._children[key].defined) { - serializer.property(key, () => this._children[key].serialize(serializer, value[key])); - } - } else if (this._schema['additionalProperties']) { - // Fallback to direct value output for additional properties - serializer.property(key, () => serializer.outputValue(value[key])); - } - } - }); + set(v: any, force = false) { + return this._set(v, false, force); } - get children() { return this._children; } + get frozen(): boolean { return this._frozen; } + + get children(): { [key: string]: SchemaTreeNode } | null { return this._children; } get type() { return 'object'; } + get tsType() { return Object; } + get defaultValue(): any | null { return null; } + + isCompatible(v: any) { return typeof v == 'object' && v !== null; } + isChildRequired(name: string) { + if (this._schema['required']) { + return this._schema['required'].indexOf(name) != -1; + } + return false; + } + + serialize(serializer: Serializer) { serializer.object(this); } } @@ -236,14 +320,28 @@ export class ObjectSchemaTreeNode extends NonLeafSchemaTreeNode<{[key: string]: export class ArraySchemaTreeNode extends NonLeafSchemaTreeNode> { // The map of all items metadata. protected _items: SchemaTreeNode[]; + protected _itemPrototype: SchemaTreeNode; constructor(metaData: TreeNodeConstructorArgument>) { super(metaData); + this._set(metaData.value, true, false); + + // Keep the item's schema as a schema node. This is important to keep type information. + this._itemPrototype = this._createChildProperty('', null, null, metaData.schema['items']); + } + + _set(value: any, init: boolean, force: boolean) { + const schema = this._schema; + const forward = this._forward; + + this._defined = !!value; + this._value = Object.create(null); + this._dirty = this._dirty || !init; - let { value, forward, schema } = metaData; if (value) { this._defined = true; } else { + this._defined = false; value = []; } this._items = []; @@ -253,26 +351,24 @@ export class ArraySchemaTreeNode extends NonLeafSchemaTreeNode> { this._items[index] = this._createChildProperty( '' + index, value && value[index], - forward && (forward as ArraySchemaTreeNode).children[index], + forward && (forward as ArraySchemaTreeNode).items[index], schema['items'] ); } + } - if (!schema['additionalProperties']) { - Object.freeze(this._value); - } + set(v: any, force = false) { + return this._set(v, false, force); } - get children() { return this._items as {[key: string]: any}; } + isCompatible(v: any) { return Array.isArray(v); } get type() { return 'array'; } + get tsType() { return Array; } + get items(): SchemaTreeNode[] { return this._items; } + get itemPrototype(): SchemaTreeNode { return this._itemPrototype; } + get defaultValue(): any | null { return null; } - serialize(serializer: Serializer, value = this._value) { - serializer.array(() => { - for (let i = 0; i < value.length; i++) { - this._items[i].serialize(serializer, value[i]); - } - }); - } + serialize(serializer: Serializer) { serializer.array(this); } } @@ -314,57 +410,61 @@ export abstract class LeafSchemaTreeNode extends SchemaTreeNode { } return this._value === undefined ? undefined : this.convert(this._value); } - set(v: T) { this.dirty = true; this._value = this.convert(v); } + set(v: T, force = false) { + if (this.readOnly && !force) { + throw new SettingReadOnlyPropertyError(); + } + + this.dirty = true; + this._value = this.convert(v); + } destroy() { this._defined = false; this._value = null; } + get defaultValue(): T { + return 'default' in this._schema ? this._default : null; + } + abstract convert(v: any): T; + abstract isCompatible(v: any): boolean; - serialize(serializer: Serializer, value: T = this.get()) { - if (this.defined) { - serializer.outputValue(value); - } + serialize(serializer: Serializer) { + serializer.outputValue(this); } } /** Basic primitives for JSON Schema. */ class StringSchemaTreeNode extends LeafSchemaTreeNode { - serialize(serializer: Serializer, value: string = this.get()) { - if (this.defined) { - serializer.outputString(value); - } - } + serialize(serializer: Serializer) { serializer.outputString(this); } + isCompatible(v: any) { return typeof v == 'string' || v instanceof String; } convert(v: any) { return v === undefined ? undefined : '' + v; } get type() { return 'string'; } + get tsType() { return String; } } class BooleanSchemaTreeNode extends LeafSchemaTreeNode { - serialize(serializer: Serializer, value: boolean = this.get()) { - if (this.defined) { - serializer.outputBoolean(value); - } - } + serialize(serializer: Serializer) { serializer.outputBoolean(this); } + isCompatible(v: any) { return typeof v == 'boolean' || v instanceof Boolean; } convert(v: any) { return v === undefined ? undefined : !!v; } get type() { return 'boolean'; } + get tsType() { return Boolean; } } class NumberSchemaTreeNode extends LeafSchemaTreeNode { - serialize(serializer: Serializer, value: number = this.get()) { - if (this.defined) { - serializer.outputNumber(value); - } - } + serialize(serializer: Serializer) { serializer.outputNumber(this); } + isCompatible(v: any) { return typeof v == 'number' || v instanceof Number; } convert(v: any) { return v === undefined ? undefined : +v; } get type() { return 'number'; } + get tsType() { return Number; } } diff --git a/packages/@ngtools/json-schema/src/serializer.ts b/packages/@ngtools/json-schema/src/serializer.ts new file mode 100644 index 000000000000..ddcb5d7f6c1b --- /dev/null +++ b/packages/@ngtools/json-schema/src/serializer.ts @@ -0,0 +1,26 @@ +import {JsonSchemaErrorBase} from './error'; +import {SchemaNode} from './node'; +export class InvalidStateError extends JsonSchemaErrorBase {} + + +export interface WriterFn { + (str: string): void; +} + +export abstract class Serializer { + abstract start(): void; + abstract end(): void; + + abstract object(node: SchemaNode): void; + abstract property(node: SchemaNode): void; + abstract array(node: SchemaNode): void; + + abstract outputOneOf(node: SchemaNode): void; + + abstract outputString(node: SchemaNode): void; + abstract outputNumber(node: SchemaNode): void; + abstract outputBoolean(node: SchemaNode): void; + + // Fallback when the value does not have metadata. + abstract outputValue(node: SchemaNode): void; +} diff --git a/packages/@ngtools/json-schema/src/serializers/dts.spec.ts b/packages/@ngtools/json-schema/src/serializers/dts.spec.ts new file mode 100644 index 000000000000..cb290690ab1b --- /dev/null +++ b/packages/@ngtools/json-schema/src/serializers/dts.spec.ts @@ -0,0 +1,30 @@ +import * as path from 'path'; +import * as fs from 'fs'; + +import {DTsSerializer} from './dts'; +import {SchemaClassFactory} from '../schema-class-factory'; +import {RootSchemaTreeNode} from '../schema-tree'; + + +describe('DtsSerializer', () => { + const schemaJsonFilePath = path.join(__dirname, '../../tests/schema1.json'); + const schemaJson = JSON.parse(fs.readFileSync(schemaJsonFilePath, 'utf-8')); + const schemaClass = new (SchemaClassFactory(schemaJson))({}); + const schema: RootSchemaTreeNode = schemaClass.$$schema(); + + it('works', () => { + let str = ''; + function writer(s: string) { + str += s; + } + + const serializer = new DTsSerializer(writer, 'HelloWorld'); + + serializer.start(); + schema.serialize(serializer); + serializer.end(); + + // Expect optional properties to be followed by `?` + expect(str).toMatch(/stringKey\?/); + }); +}); diff --git a/packages/@ngtools/json-schema/src/serializers/dts.ts b/packages/@ngtools/json-schema/src/serializers/dts.ts new file mode 100644 index 000000000000..16029edf9ed0 --- /dev/null +++ b/packages/@ngtools/json-schema/src/serializers/dts.ts @@ -0,0 +1,150 @@ +import {SchemaNode} from '../node'; +import {Serializer, WriterFn, InvalidStateError} from '../serializer'; + + +interface DTsSerializerState { + empty?: boolean; + type?: string; + property?: boolean; +} + +export class DTsSerializer implements Serializer { + private _state: DTsSerializerState[] = []; + + constructor(private _writer: WriterFn, private interfaceName?: string, private _indentDelta = 4) { + if (interfaceName) { + _writer(`export interface ${interfaceName} `); + } else { + _writer('export default interface '); + } + } + + private _willOutputValue() { + if (this._state.length > 0) { + const top = this._top(); + top.empty = false; + + if (!top.property) { + this._indent(); + } + } + } + + private _top(): DTsSerializerState { + return this._state[this._state.length - 1] || {}; + } + + private _indent(): string { + if (this._indentDelta == 0) { + return; + } + + let str = '\n'; + let i = this._state.length * this._indentDelta; + while (i--) { + str += ' '; + } + this._writer(str); + } + + start() {} + end() { + if (this._indentDelta) { + this._writer('\n'); + } + } + + object(node: SchemaNode) { + this._willOutputValue(); + + this._writer('{'); + + this._state.push({ empty: true, type: 'object' }); + for (const key of Object.keys(node.children)) { + this.property(node.children[key]); + } + + // Fallback to direct value output for additional properties. + if (!node.frozen) { + this._indent(); + this._writer('[name: string]: any;'); + } + this._state.pop(); + + if (!this._top().empty) { + this._indent(); + } + this._writer('}'); + } + + property(node: SchemaNode) { + this._willOutputValue(); + + if (node.description) { + this._writer('/**'); + this._indent(); + node.description.split('\n').forEach(line => { + this._writer(' * ' + line); + this._indent(); + }); + this._writer(' */'); + this._indent(); + } + + this._writer(node.name); + if (!node.required) { + this._writer('?'); + } + + this._writer(': '); + this._top().property = true; + node.serialize(this); + this._top().property = false; + this._writer(';'); + } + + array(node: SchemaNode) { + this._willOutputValue(); + + node.itemPrototype.serialize(this); + this._writer('[]'); + } + + outputOneOf(node: SchemaNode) { + this._willOutputValue(); + if (!node.items) { + throw new InvalidStateError(); + } + + this._writer('('); + for (let i = 0; i < node.items.length; i++) { + node.items[i].serialize(this); + if (i != node.items.length - 1) { + this._writer(' | '); + } + } + this._writer(')'); + } + + outputValue(node: SchemaNode) { + this._willOutputValue(); + this._writer('any'); + } + + outputString(node: SchemaNode) { + this._willOutputValue(); + this._writer('string'); + } + outputNumber(node: SchemaNode) { + this._willOutputValue(); + this._writer('number'); + } + outputInteger(node: SchemaNode) { + this._willOutputValue(); + this._writer('number'); + } + outputBoolean(node: SchemaNode) { + this._willOutputValue(); + this._writer('boolean'); + } +} diff --git a/packages/@ngtools/json-schema/src/serializers/json.spec.ts b/packages/@ngtools/json-schema/src/serializers/json.spec.ts new file mode 100644 index 000000000000..ef6f36563dfb --- /dev/null +++ b/packages/@ngtools/json-schema/src/serializers/json.spec.ts @@ -0,0 +1,32 @@ +import * as path from 'path'; +import * as fs from 'fs'; + +import {JsonSerializer} from './json'; +import {SchemaClassFactory} from '../schema-class-factory'; +import {RootSchemaTreeNode} from '../schema-tree'; + + +describe('JsonSerializer', () => { + const schemaJsonFilePath = path.join(__dirname, '../../tests/schema1.json'); + const schemaJson = JSON.parse(fs.readFileSync(schemaJsonFilePath, 'utf-8')); + const valueJsonFilePath = path.join(__dirname, '../../tests/value1.json'); + const valueJson = JSON.parse(fs.readFileSync(valueJsonFilePath, 'utf-8')); + + const schemaClass = new (SchemaClassFactory(schemaJson))(valueJson); + const schema: RootSchemaTreeNode = schemaClass.$$schema(); + + it('works', () => { + let str = ''; + function writer(s: string) { + str += s; + } + + const serializer = new JsonSerializer(writer); + + serializer.start(); + schema.serialize(serializer); + serializer.end(); + + expect(JSON.stringify(JSON.parse(str))).toEqual(JSON.stringify(valueJson)); + }); +}); diff --git a/packages/angular-cli/models/json-schema/serializer.ts b/packages/@ngtools/json-schema/src/serializers/json.ts similarity index 50% rename from packages/angular-cli/models/json-schema/serializer.ts rename to packages/@ngtools/json-schema/src/serializers/json.ts index bee8c9645cf3..1ab57575ffd7 100644 --- a/packages/angular-cli/models/json-schema/serializer.ts +++ b/packages/@ngtools/json-schema/src/serializers/json.ts @@ -1,39 +1,5 @@ -import {NgToolkitError} from '../error'; -export class InvalidStateError extends NgToolkitError {} -export class UnknownMimetype extends NgToolkitError {} - - -export interface WriterFn { - (str: string): void; -} - -export abstract class Serializer { - abstract start(): void; - abstract end(): void; - - abstract object(callback: () => void): void; - abstract property(name: string, callback: () => void): void; - abstract array(callback: () => void): void; - - abstract outputString(value: string): void; - abstract outputNumber(value: number): void; - abstract outputBoolean(value: boolean): void; - - // Fallback when the value does not have metadata. - abstract outputValue(value: any): void; - - - static fromMimetype(mimetype: string, writer: WriterFn, ...opts: any[]): Serializer { - let Klass: { new(writer: WriterFn, ...args: any[]): Serializer } = null; - switch (mimetype) { - case 'application/json': Klass = JsonSerializer; break; - - default: throw new UnknownMimetype(); - } - - return new Klass(writer, ...opts); - } -} +import {SchemaNode} from '../node'; +import {Serializer, WriterFn} from '../serializer'; interface JsonSerializerState { @@ -42,7 +8,7 @@ interface JsonSerializerState { property?: boolean; } -class JsonSerializer implements Serializer { +export class JsonSerializer implements Serializer { private _state: JsonSerializerState[] = []; constructor(private _writer: WriterFn, private _indentDelta = 2) {} @@ -87,13 +53,34 @@ class JsonSerializer implements Serializer { } } - object(callback: () => void) { + object(node: SchemaNode) { + if (node.defined == false) { + return; + } + this._willOutputValue(); this._writer('{'); - this._state.push({ empty: true, type: 'object' }); - callback(); + + for (const key of Object.keys(node.children)) { + this.property(node.children[key]); + } + + // Fallback to direct value output for additional properties. + if (!node.frozen) { + for (const key of Object.keys(node.value)) { + if (key in node.children) { + continue; + } + + this._willOutputValue(); + this._writer(JSON.stringify(key)); + this._writer(': '); + this._writer(JSON.stringify(node.value)); + } + } + this._state.pop(); if (!this._top().empty) { @@ -102,22 +89,32 @@ class JsonSerializer implements Serializer { this._writer('}'); } - property(name: string, callback: () => void) { + property(node: SchemaNode) { + if (node.defined == false) { + return; + } + this._willOutputValue(); - this._writer(JSON.stringify(name)); + this._writer(JSON.stringify(node.name)); this._writer(': '); this._top().property = true; - callback(); + node.serialize(this); this._top().property = false; } - array(callback: () => void) { + array(node: SchemaNode) { + if (node.defined == false) { + return; + } + this._willOutputValue(); this._writer('['); this._state.push({ empty: true, type: 'array' }); - callback(); + for (let i = 0; i < node.items.length; i++) { + node.items[i].serialize(this); + } this._state.pop(); if (!this._top().empty) { @@ -126,25 +123,29 @@ class JsonSerializer implements Serializer { this._writer(']'); } - outputValue(value: any) { + outputOneOf(node: SchemaNode) { + this.outputValue(node); + } + + outputValue(node: SchemaNode) { this._willOutputValue(); - this._writer(JSON.stringify(value, null, this._indentDelta)); + this._writer(JSON.stringify(node.value, null, this._indentDelta)); } - outputString(value: string) { + outputString(node: SchemaNode) { this._willOutputValue(); - this._writer(JSON.stringify(value)); + this._writer(JSON.stringify(node.value)); } - outputNumber(value: number) { + outputNumber(node: SchemaNode) { this._willOutputValue(); - this._writer(JSON.stringify(value)); + this._writer(JSON.stringify(node.value)); } - outputInteger(value: number) { + outputInteger(node: SchemaNode) { this._willOutputValue(); - this._writer(JSON.stringify(value)); + this._writer(JSON.stringify(node.value)); } - outputBoolean(value: boolean) { + outputBoolean(node: SchemaNode) { this._willOutputValue(); - this._writer(JSON.stringify(value)); + this._writer(JSON.stringify(node.value)); } } diff --git a/packages/@ngtools/json-schema/tests/schema1.json b/packages/@ngtools/json-schema/tests/schema1.json new file mode 100644 index 000000000000..983feceb8bcd --- /dev/null +++ b/packages/@ngtools/json-schema/tests/schema1.json @@ -0,0 +1,84 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "JsonSchema", + "type": "object", + "properties": { + "requiredKey": { + "type": "number" + }, + "stringKeyDefault": { + "type": "string", + "default": "defaultValue" + }, + "stringKey": { + "type": "string" + }, + "booleanKey": { + "type": "boolean" + }, + "numberKey": { + "type": "number" + }, + "oneOfKey1": { + "oneOf": [ + { "type": "string" }, + { "type": "number" } + ] + }, + "oneOfKey2": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, + "objectKey1": { + "type": "object", + "properties": { + "stringKey": { + "type": "string" + }, + "objectKey": { + "type": "object", + "properties": { + "stringKey": { + "type": "string" + } + } + } + } + }, + "objectKey2": { + "type": "object", + "properties": { + "stringKey": { + "type": "string", + "default": "default objectKey2.stringKey" + } + }, + "additionalProperties": true + }, + "arrayKey1": { + "type": "array", + "items": { + "type": "object", + "properties": { + "stringKey": { + "type": "string" + } + } + } + }, + "arrayKey2": { + "type": "array", + "items": { + "type": "object", + "properties": { + "stringKey": { + "type": "string" + } + } + } + } + }, + "required": ["requiredKey"] +} \ No newline at end of file diff --git a/packages/@ngtools/json-schema/tests/value1-1.json b/packages/@ngtools/json-schema/tests/value1-1.json new file mode 100644 index 000000000000..4f2d08d38fb4 --- /dev/null +++ b/packages/@ngtools/json-schema/tests/value1-1.json @@ -0,0 +1,8 @@ +{ + "requiredKey": 1, + "arrayKey2": [ + { "stringKey": "value1" }, + { "stringKey": "value2" } + ], + "oneOfKey2": [ "hello", "world" ] +} \ No newline at end of file diff --git a/packages/@ngtools/json-schema/tests/value1.json b/packages/@ngtools/json-schema/tests/value1.json new file mode 100644 index 000000000000..31f049534148 --- /dev/null +++ b/packages/@ngtools/json-schema/tests/value1.json @@ -0,0 +1,7 @@ +{ + "requiredKey": 1, + "arrayKey2": [ + { "stringKey": "value1" }, + { "stringKey": "value2" } + ] +} \ No newline at end of file diff --git a/packages/@ngtools/json-schema/tsconfig.json b/packages/@ngtools/json-schema/tsconfig.json new file mode 100644 index 000000000000..1e9923f52165 --- /dev/null +++ b/packages/@ngtools/json-schema/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "declaration": true, + "experimentalDecorators": true, + "mapRoot": "", + "module": "commonjs", + "moduleResolution": "node", + "noEmitOnError": true, + "noImplicitAny": true, + "outDir": "../../../dist/@ngtools/json-schema", + "rootDir": ".", + "lib": ["es2015", "es6", "dom"], + "target": "es5", + "sourceMap": true, + "sourceRoot": "/", + "baseUrl": "./", + "paths": { + }, + "typeRoots": [ + "../../node_modules/@types" + ], + "types": [ + "jasmine", + "node" + ] + } +} diff --git a/packages/angular-cli/lib/config/.gitignore b/packages/angular-cli/lib/config/.gitignore new file mode 100644 index 000000000000..879ebeae0a5f --- /dev/null +++ b/packages/angular-cli/lib/config/.gitignore @@ -0,0 +1 @@ +schema.d.ts \ No newline at end of file diff --git a/packages/angular-cli/lib/config/schema.d.ts b/packages/angular-cli/lib/config/schema.d.ts deleted file mode 100644 index d323223b5f36..000000000000 --- a/packages/angular-cli/lib/config/schema.d.ts +++ /dev/null @@ -1,79 +0,0 @@ -export interface CliConfig { - /** - * The global configuration of the project. - */ - project?: { - version?: string; - name?: string; - }; - /** - * Properties of the different applications in this project. - */ - apps?: { - root?: string; - outDir?: string; - assets?: string; - deployUrl?: string; - index?: string; - main?: string; - test?: string; - tsconfig?: string; - prefix?: string; - mobile?: boolean; - /** - * Global styles to be included in the build. - */ - styles?: string[]; - /** - * Global scripts to be included in the build. - */ - scripts?: string[]; - /** - * Name and corresponding file for environment config. - */ - environments?: { - [name: string]: any; - }; - }[]; - /** - * Configuration reserved for installed third party addons. - */ - addons?: { - [name: string]: any; - }[]; - /** - * Configuration reserved for installed third party packages. - */ - packages?: { - [name: string]: any; - }[]; - e2e?: { - protractor?: { - config?: string; - }; - }; - test?: { - karma?: { - config?: string; - }; - }; - defaults?: { - styleExt?: string; - prefixInterfaces?: boolean; - poll?: number; - viewEncapsulation?: string; - changeDetection?: string; - inline?: { - style?: boolean; - template?: boolean; - }; - spec?: { - class?: boolean; - component?: boolean; - directive?: boolean; - module?: boolean; - pipe?: boolean; - service?: boolean; - }; - }; -} diff --git a/packages/angular-cli/lib/config/schema.json b/packages/angular-cli/lib/config/schema.json index 3a2a1f02fcc9..c8626d1aac1d 100644 --- a/packages/angular-cli/lib/config/schema.json +++ b/packages/angular-cli/lib/config/schema.json @@ -104,7 +104,8 @@ "type": "string" } }, - "additionalProperties": true + "additionalProperties": true, + "required": ["input"] } ] }, diff --git a/tests/models/config.spec.ts b/packages/angular-cli/models/config/config.spec.ts similarity index 50% rename from tests/models/config.spec.ts rename to packages/angular-cli/models/config/config.spec.ts index ece9ee4f5969..2c6c78eb8744 100644 --- a/tests/models/config.spec.ts +++ b/packages/angular-cli/models/config/config.spec.ts @@ -1,10 +1,8 @@ -import {CliConfig} from 'angular-cli/models/config/config'; +import {CliConfig} from './config'; import * as fs from 'fs'; import * as path from 'path'; import {CliConfig as ConfigInterface} from './spec-schema'; -const expect = require('chai').expect; - describe('Config', () => { let schema = JSON.parse(fs.readFileSync(path.join(__dirname, 'spec-schema.json'), 'utf-8')); @@ -16,17 +14,17 @@ describe('Config', () => { }); const config = cliConfig.config; - expect(config.requiredKey).to.equal(1); - expect(config.stringKey).to.equal('stringValue'); - expect(config.stringKeyDefault).to.equal('defaultValue'); - expect(config.booleanKey).to.equal(undefined); + expect(config.requiredKey).toEqual(1); + expect(config.stringKey).toEqual('stringValue'); + expect(config.stringKeyDefault).toEqual('defaultValue'); + expect(config.booleanKey).toEqual(undefined); - expect(config.arrayKey1).to.equal(undefined); - expect(() => config.arrayKey1[0]).to.throw(); + expect(config.arrayKey1).toEqual(undefined); + expect(() => config.arrayKey1[0]).toThrow(); - expect(config.numberKey).to.equal(undefined); + expect(config.numberKey).toEqual(undefined); config.numberKey = 33; - expect(config.numberKey).to.equal(33); + expect(config.numberKey).toEqual(33); }); describe('Get', () => { @@ -36,9 +34,9 @@ describe('Config', () => { stringKey: 'stringValue' }); - expect(config.get('requiredKey')).to.equal(1); - expect(config.get('stringKey')).to.equal('stringValue'); - expect(config.get('booleanKey')).to.equal(undefined); + expect(config.get('requiredKey')).toEqual(1); + expect(config.get('stringKey')).toEqual('stringValue'); + expect(config.get('booleanKey')).toEqual(undefined); }); it('will never throw', () => { @@ -46,10 +44,10 @@ describe('Config', () => { requiredKey: 1 }); - expect(config.get('arrayKey1')).to.equal(undefined); - expect(config.get('arrayKey2[0]')).to.equal(undefined); - expect(config.get('arrayKey2[0].stringKey')).to.equal(undefined); - expect(config.get('arrayKey2[0].stringKey.a.b.c.d')).to.equal(undefined); + expect(config.get('arrayKey1')).toEqual(undefined); + expect(config.get('arrayKey2[0]')).toEqual(undefined); + expect(config.get('arrayKey2[0].stringKey')).toEqual(undefined); + expect(config.get('arrayKey2[0].stringKey.a.b.c.d')).toEqual(undefined); }); }); @@ -63,28 +61,28 @@ describe('Config', () => { ); // Check on string. - expect(cliConfig.isDefined('stringKey')).to.equal(false); - expect(cliConfig.config.stringKey).to.equal('stringValue'); + expect(cliConfig.isDefined('stringKey')).toEqual(false); + expect(cliConfig.config.stringKey).toEqual('stringValue'); cliConfig.config.stringKey = 'stringValue2'; - expect(cliConfig.isDefined('stringKey')).to.equal(true); - expect(cliConfig.config.stringKey).to.equal('stringValue2'); + expect(cliConfig.isDefined('stringKey')).toEqual(true); + expect(cliConfig.config.stringKey).toEqual('stringValue2'); cliConfig.deletePath('stringKey'); - expect(cliConfig.isDefined('stringKey')).to.equal(false); - expect(cliConfig.config.stringKey).to.equal('stringValue'); + expect(cliConfig.isDefined('stringKey')).toEqual(false); + expect(cliConfig.config.stringKey).toEqual('stringValue'); // Check on number (which is 2 fallbacks behind) - expect(cliConfig.isDefined('numberKey')).to.equal(false); - expect(cliConfig.config.numberKey).to.equal(1); + expect(cliConfig.isDefined('numberKey')).toEqual(false); + expect(cliConfig.config.numberKey).toEqual(1); cliConfig.config.numberKey = 2; - expect(cliConfig.isDefined('numberKey')).to.equal(true); - expect(cliConfig.config.numberKey).to.equal(2); + expect(cliConfig.isDefined('numberKey')).toEqual(true); + expect(cliConfig.config.numberKey).toEqual(2); cliConfig.deletePath('numberKey'); - expect(cliConfig.isDefined('numberKey')).to.equal(false); - expect(cliConfig.config.numberKey).to.equal(1); + expect(cliConfig.isDefined('numberKey')).toEqual(false); + expect(cliConfig.config.numberKey).toEqual(1); }); it('saves', () => { @@ -100,7 +98,7 @@ describe('Config', () => { ] ); - expect(cliConfig.config.arrayKey2[0].stringKey).to.equal('value1'); - expect(JSON.stringify(JSON.parse(cliConfig.serialize()))).to.equal(JSON.stringify(jsonObject)); + expect(cliConfig.config.arrayKey2[0].stringKey).toEqual('value1'); + expect(JSON.stringify(JSON.parse(cliConfig.serialize()))).toEqual(JSON.stringify(jsonObject)); }); }); diff --git a/packages/angular-cli/models/config/config.ts b/packages/angular-cli/models/config/config.ts index 4968747f7cba..ba98d5398e2d 100644 --- a/packages/angular-cli/models/config/config.ts +++ b/packages/angular-cli/models/config/config.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import {SchemaClass, SchemaClassFactory} from '../json-schema/schema-class-factory'; +import {SchemaClass, SchemaClassFactory} from '@ngtools/json-schema'; const DEFAULT_CONFIG_SCHEMA_PATH = path.join(__dirname, '../../lib/config/schema.json'); diff --git a/tests/models/spec-schema.d.ts b/packages/angular-cli/models/config/spec-schema.d.ts similarity index 100% rename from tests/models/spec-schema.d.ts rename to packages/angular-cli/models/config/spec-schema.d.ts diff --git a/tests/models/spec-schema.json b/packages/angular-cli/models/config/spec-schema.json similarity index 91% rename from tests/models/spec-schema.json rename to packages/angular-cli/models/config/spec-schema.json index af3648c9fc27..9922d87cba96 100644 --- a/tests/models/spec-schema.json +++ b/packages/angular-cli/models/config/spec-schema.json @@ -39,7 +39,8 @@ "type": "object", "properties": { "stringKey": { - "type": "string" + "type": "string", + "default": "default objectKey2.stringKey" } }, "additionalProperties": true @@ -47,9 +48,7 @@ "arrayKey1": { "type": "array", "items": { - "stringKey": { - "type": "string" - } + "type": "string" } }, "arrayKey2": { diff --git a/packages/angular-cli/package.json b/packages/angular-cli/package.json index f7a22fa0cbad..316a8e173405 100644 --- a/packages/angular-cli/package.json +++ b/packages/angular-cli/package.json @@ -30,6 +30,7 @@ "@angular/compiler": "^2.3.1", "@angular/compiler-cli": "^2.3.1", "@angular/core": "^2.3.1", + "@ngtools/json-schema": "^1.0.0", "@ngtools/webpack": "^1.0.0", "async": "^2.1.4", "autoprefixer": "^6.5.3", diff --git a/packages/angular-cli/tsconfig.json b/packages/angular-cli/tsconfig.json index 818030de11d5..fc88d7050536 100644 --- a/packages/angular-cli/tsconfig.json +++ b/packages/angular-cli/tsconfig.json @@ -23,6 +23,7 @@ "@angular-cli/ast-tools": [ "../../dist/@angular-cli/ast-tools/src" ], "@angular-cli/base-href-webpack": [ "../../dist/@angular-cli/base-href-webpack/src" ], "@angular-cli/version": [ "../../dist/@angular-cli/version/src" ], + "@ngtools/json-schema": [ "../../dist/@ngtools/json-schema/src" ], "@ngtools/webpack": [ "../../dist/@ngtools/webpack/src" ] } }, diff --git a/scripts/build-schema-dts.js b/scripts/build-schema-dts.js new file mode 100644 index 000000000000..3baa3608be6f --- /dev/null +++ b/scripts/build-schema-dts.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const minimist = require('minimist'); + +// Load the bootstrap. +require('../lib/bootstrap-local'); +const SchemaClassFactory = require('@ngtools/json-schema').SchemaClassFactory; + +const argv = minimist(process.argv.slice(2)); +const inFile = argv._[0]; +const outFile = argv._[1]; + +if (!inFile) { + process.stderr.write('Need to pass in an input file.\n'); + process.exit(1); +} +const jsonSchema = JSON.parse(fs.readFileSync(inFile, 'utf-8')); +const SchemaClass = SchemaClassFactory(jsonSchema); +const schemaInstance = new SchemaClass(); +const serialized = schemaInstance.$$serialize('text/x.dts', 'CliConfig'); + +if (outFile) { + fs.writeFileSync(outFile, serialized, 'utf-8'); +} else { + process.stdout.write(serialized); +} diff --git a/scripts/publish/build.js b/scripts/publish/build.js index a971efe00495..e07b8199d314 100755 --- a/scripts/publish/build.js +++ b/scripts/publish/build.js @@ -56,6 +56,13 @@ function getDeps(pkg) { Promise.resolve() .then(() => console.log('Deleting dist folder...')) .then(() => rimraf(dist)) + .then(() => console.log('Creating schema.d.ts...')) + .then(() => { + const script = path.join(root, 'scripts/build-schema-dts.js'); + const input = path.join(root, 'packages/angular-cli/lib/config/schema.json'); + const output = path.join(root, 'packages/angular-cli/lib/config/schema.d.ts'); + return npmRun.execSync(`node ${script} ${input} ${output}`); + }) .then(() => console.log('Compiling packages...')) .then(() => { const packages = require('../../lib/packages');