+ * ```
+ * contains five bindings:
+ * 1. ngFor -> null
+ * 2. item -> NgForOfContext.$implicit
+ * 3. ngForOf -> items
+ * 4. i -> NgForOfContext.index
+ * 5. ngForTrackBy -> func
+ *
+ * For a full description of the microsyntax grammar, see
+ * https://gist.github.com/mhevery/d3530294cff2e4a1b3fe15ff75d08855
+ *
+ * @param templateKey name of the microsyntax directive, like ngIf, ngFor, without the *
+ */
+ parseTemplateBindings(templateKey: string): TemplateBindingParseResult {
const bindings: TemplateBinding[] = [];
- const warnings: string[] = [];
- do {
- const start = this.inputIndex;
- let rawKey: string;
- let key: string;
- let isVar: boolean = false;
- if (firstBinding) {
- rawKey = key = tplKey;
- firstBinding = false;
- } else {
- isVar = this.peekKeywordLet();
- if (isVar) this.advance();
- rawKey = this.expectTemplateBindingKey();
- key = isVar ? rawKey : tplKey + rawKey[0].toUpperCase() + rawKey.substring(1);
- this.optionalCharacter(chars.$COLON);
- }
- let name: string = null !;
- let expression: ASTWithSource|null = null;
- if (isVar) {
- if (this.optionalOperator('=')) {
- name = this.expectTemplateBindingKey();
+ // The first binding is for the template key itself
+ // In *ngFor="let item of items", key = "ngFor", value = null
+ // In *ngIf="cond | pipe", key = "ngIf", value = "cond | pipe"
+ bindings.push(...this.parseDirectiveKeywordBindings(
+ templateKey, new ParseSpan(0, templateKey.length), this.absoluteOffset));
+
+ while (this.index < this.tokens.length) {
+ // If it starts with 'let', then this must be variable declaration
+ const letBinding = this.parseLetBinding();
+ if (letBinding) {
+ bindings.push(letBinding);
+ } else {
+ // Two possible cases here, either `value "as" key` or
+ // "directive-keyword expression". We don't know which case, but both
+ // "value" and "directive-keyword" are template binding key, so consume
+ // the key first.
+ const {key, keySpan} = this.expectTemplateBindingKey();
+ // Peek at the next token, if it is "as" then this must be variable
+ // declaration.
+ const binding = this.parseAsBinding(key, keySpan, this.absoluteOffset);
+ if (binding) {
+ bindings.push(binding);
} else {
- name = '\$implicit';
+ // Otherwise the key must be a directive keyword, like "of". Transform
+ // the key to actual key. Eg. of -> ngForOf, trackBy -> ngForTrackBy
+ const actualKey = templateKey + key[0].toUpperCase() + key.substring(1);
+ bindings.push(
+ ...this.parseDirectiveKeywordBindings(actualKey, keySpan, this.absoluteOffset));
}
- } else if (this.peekKeywordAs()) {
- this.advance(); // consume `as`
- name = rawKey;
- key = this.expectTemplateBindingKey(); // read local var name
- isVar = true;
- } else if (this.next !== EOF && !this.peekKeywordLet()) {
- const start = this.inputIndex;
- const ast = this.parsePipe();
- const source = this.input.substring(start - this.offset, this.inputIndex - this.offset);
- expression =
- new ASTWithSource(ast, source, this.location, this.absoluteOffset + start, this.errors);
}
+ this.consumeStatementTerminator();
+ }
- bindings.push(new TemplateBinding(
- this.span(start), this.sourceSpan(start), key, isVar, name, expression));
- if (this.peekKeywordAs() && !isVar) {
- const letStart = this.inputIndex;
- this.advance(); // consume `as`
- const letName = this.expectTemplateBindingKey(); // read local var name
- bindings.push(new TemplateBinding(
- this.span(letStart), this.sourceSpan(letStart), letName, true, key, null !));
- }
- if (!this.optionalCharacter(chars.$SEMICOLON)) {
- this.optionalCharacter(chars.$COMMA);
- }
- } while (this.index < this.tokens.length);
+ return new TemplateBindingParseResult(bindings, [] /* warnings */, this.errors);
+ }
- return new TemplateBindingParseResult(bindings, warnings, this.errors);
+ /**
+ * Parse a directive keyword, followed by a mandatory expression.
+ * For example, "of items", "trackBy: func".
+ * The bindings are: ngForOf -> items, ngForTrackBy -> func
+ * There could be an optional "as" binding that follows the expression.
+ * For example,
+ * ```
+ * *ngFor="let item of items | slice:0:1 as collection".`
+ * ^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^
+ * keyword bound target optional 'as' binding
+ * ```
+ *
+ * @param key binding key, for example, ngFor, ngIf, ngForOf
+ * @param keySpan span of the key in the expression. keySpan might be different
+ * from `key.length`. For example, the span for key "ngForOf" is "of".
+ * @param absoluteOffset absolute offset of the attribute value
+ */
+ private parseDirectiveKeywordBindings(key: string, keySpan: ParseSpan, absoluteOffset: number):
+ TemplateBinding[] {
+ const bindings: TemplateBinding[] = [];
+ this.consumeOptionalCharacter(chars.$COLON); // trackBy: trackByFunction
+ const valueExpr = this.getDirectiveBoundTarget();
+ const span = new ParseSpan(keySpan.start, this.inputIndex);
+ bindings.push(new TemplateBinding(
+ span, span.toAbsolute(absoluteOffset), key, false /* keyIsVar */, valueExpr?.source || '', valueExpr));
+ // The binding could optionally be followed by "as". For example,
+ // *ngIf="cond | pipe as x". In this case, the key in the "as" binding
+ // is "x" and the value is the template key itself ("ngIf"). Note that the
+ // 'key' in the current context now becomes the "value" in the next binding.
+ const asBinding = this.parseAsBinding(key, keySpan, absoluteOffset);
+ if (asBinding) {
+ bindings.push(asBinding);
+ }
+ this.consumeStatementTerminator();
+ return bindings;
+ }
+
+ /**
+ * Return the expression AST for the bound target of a directive keyword
+ * binding. For example,
+ * ```
+ * *ngIf="condition | pipe".
+ * ^^^^^^^^^^^^^^^^ bound target for "ngIf"
+ * *ngFor="let item of items"
+ * ^^^^^ bound target for "ngForOf"
+ * ```
+ */
+ private getDirectiveBoundTarget(): ASTWithSource|null {
+ if (this.next === EOF || this.peekKeywordAs() || this.peekKeywordLet()) {
+ return null;
+ }
+ const ast = this.parsePipe(); // example: "condition | async"
+ const {start, end} = ast.span;
+ const value = this.input.substring(start, end);
+ return new ASTWithSource(ast, value, this.location, this.absoluteOffset + start, this.errors);
+ }
+
+ /**
+ * Return the binding for a variable declared using `as`. Note that the order
+ * of the key-value pair in this declaration is reversed. For example,
+ * ```
+ * *ngFor="let item of items; index as i"
+ * ^^^^^ ^
+ * value key
+ * ```
+ *
+ * @param value name of the value in the declaration, "ngIf" in the example above
+ * @param valueSpan span of the value in the declaration
+ * @param absoluteOffset absolute offset of `value`
+ */
+ private parseAsBinding(value: string, valueSpan: ParseSpan, absoluteOffset: number):
+ TemplateBinding|null {
+ if (!this.peekKeywordAs()) {
+ return null;
+ }
+ this.advance(); // consume the 'as' keyword
+ const {key} = this.expectTemplateBindingKey();
+ const valueAst = new AST(valueSpan, valueSpan.toAbsolute(absoluteOffset));
+ const valueExpr = new ASTWithSource(
+ valueAst, value, this.location, absoluteOffset + valueSpan.start, this.errors);
+ const span = new ParseSpan(valueSpan.start, this.inputIndex);
+ return new TemplateBinding(
+ span, span.toAbsolute(absoluteOffset), key, true /* keyIsVar */, value, valueExpr);
+ }
+
+ /**
+ * Return the binding for a variable declared using `let`. For example,
+ * ```
+ * *ngFor="let item of items; let i=index;"
+ * ^^^^^^^^ ^^^^^^^^^^^
+ * ```
+ * In the first binding, `item` is bound to `NgForOfContext.$implicit`.
+ * In the second binding, `i` is bound to `NgForOfContext.index`.
+ */
+ private parseLetBinding(): TemplateBinding|null {
+ if (!this.peekKeywordLet()) {
+ return null;
+ }
+ const spanStart = this.inputIndex;
+ this.advance(); // consume the 'let' keyword
+ const {key} = this.expectTemplateBindingKey();
+ let valueExpr: ASTWithSource|null = null;
+ if (this.consumeOptionalOperator('=')) {
+ const {key: value, keySpan: valueSpan} = this.expectTemplateBindingKey();
+ const ast = new AST(valueSpan, valueSpan.toAbsolute(this.absoluteOffset));
+ valueExpr = new ASTWithSource(
+ ast, value, this.location, this.absoluteOffset + valueSpan.start, this.errors);
+ }
+ const spanEnd = this.inputIndex;
+ const span = new ParseSpan(spanStart, spanEnd);
+ return new TemplateBinding(
+ span, span.toAbsolute(this.absoluteOffset), key, true /* keyIsVar */, valueExpr?.source || '$implicit', valueExpr);
+ }
+
+ /**
+ * Consume the optional statement terminator: semicolon or comma.
+ */
+ private consumeStatementTerminator() {
+ this.consumeOptionalCharacter(chars.$SEMICOLON) || this.consumeOptionalCharacter(chars.$COMMA);
}
error(message: string, index: number|null = null) {
@@ -896,4 +1046,4 @@ class IvySimpleExpressionChecker extends SimpleExpressionChecker {
}
visitPrefixNot(ast: PrefixNot, context: any) { ast.expression.visit(this); }
-}
\ No newline at end of file
+}
diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts
index e105af2d17ca..a481bdb67482 100644
--- a/packages/compiler/src/render3/view/compiler.ts
+++ b/packages/compiler/src/render3/view/compiler.ts
@@ -87,7 +87,7 @@ function baseDirectiveFields(
*/
function addFeatures(
definitionMap: DefinitionMap, meta: R3DirectiveMetadata | R3ComponentMetadata) {
- // e.g. `features: [NgOnChangesFeature()]`
+ // e.g. `features: [NgOnChangesFeature]`
const features: o.Expression[] = [];
const providers = meta.providers;
@@ -107,7 +107,7 @@ function addFeatures(
features.push(o.importExpr(R3.CopyDefinitionFeature));
}
if (meta.lifecycle.usesOnChanges) {
- features.push(o.importExpr(R3.NgOnChangesFeature).callFn(EMPTY_ARRAY));
+ features.push(o.importExpr(R3.NgOnChangesFeature));
}
if (features.length) {
definitionMap.set('features', o.literalArr(features));
diff --git a/packages/compiler/src/template_parser/binding_parser.ts b/packages/compiler/src/template_parser/binding_parser.ts
index 6ea53eab0e15..bfb311a9a617 100644
--- a/packages/compiler/src/template_parser/binding_parser.ts
+++ b/packages/compiler/src/template_parser/binding_parser.ts
@@ -134,10 +134,9 @@ export class BindingParser {
const binding = bindings[i];
if (binding.keyIsVar) {
targetVars.push(new ParsedVariable(binding.key, binding.name, sourceSpan));
- } else if (binding.expression) {
+ } else if (binding.value) {
this._parsePropertyAst(
- binding.key, binding.expression, sourceSpan, undefined, targetMatchableAttrs,
- targetProps);
+ binding.key, binding.value, sourceSpan, undefined, targetMatchableAttrs, targetProps);
} else {
targetMatchableAttrs.push([binding.key, '']);
this.parseLiteralAttr(
@@ -165,8 +164,8 @@ export class BindingParser {
this._exprParser.parseTemplateBindings(tplKey, tplValue, sourceInfo, absoluteValueOffset);
this._reportExpressionParserErrors(bindingsResult.errors, sourceSpan);
bindingsResult.templateBindings.forEach((binding) => {
- if (binding.expression) {
- this._checkPipes(binding.expression, sourceSpan);
+ if (binding.value) {
+ this._checkPipes(binding.value, sourceSpan);
}
});
bindingsResult.warnings.forEach(
diff --git a/packages/compiler/test/expression_parser/parser_spec.ts b/packages/compiler/test/expression_parser/parser_spec.ts
index fb5ac235309d..86ba14941839 100644
--- a/packages/compiler/test/expression_parser/parser_spec.ts
+++ b/packages/compiler/test/expression_parser/parser_spec.ts
@@ -248,14 +248,16 @@ describe('parser', () => {
describe('parseTemplateBindings', () => {
- function keys(templateBindings: any[]) { return templateBindings.map(binding => binding.key); }
+ function keys(templateBindings: TemplateBinding[]) {
+ return templateBindings.map(binding => binding.key);
+ }
- function keyValues(templateBindings: any[]) {
+ function keyValues(templateBindings: TemplateBinding[]) {
return templateBindings.map(binding => {
if (binding.keyIsVar) {
return 'let ' + binding.key + (binding.name == null ? '=null' : '=' + binding.name);
} else {
- return binding.key + (binding.expression == null ? '' : `=${binding.expression}`);
+ return binding.key + (binding.value == null ? '' : `=${binding.value}`);
}
});
}
@@ -265,9 +267,16 @@ describe('parser', () => {
binding => source.substring(binding.span.start, binding.span.end));
}
- function exprSources(templateBindings: any[]) {
- return templateBindings.map(
- binding => binding.expression != null ? binding.expression.source : null);
+ function exprSources(templateBindings: TemplateBinding[]) {
+ return templateBindings.map(binding => binding.value != null ? binding.value.source : null);
+ }
+
+ function humanize(bindings: TemplateBinding[]): Array<[string, string | null, boolean]> {
+ return bindings.map(binding => {
+ const {key, value: expression, name, keyIsVar} = binding;
+ const value = keyIsVar ? name : (expression ? expression.source : expression);
+ return [key, value, keyIsVar];
+ });
}
it('should parse a key without a value',
@@ -308,13 +317,51 @@ describe('parser', () => {
it('should store the sources in the result', () => {
const bindings = parseTemplateBindings('a', '1,b 2');
- expect(bindings[0].expression !.source).toEqual('1');
- expect(bindings[1].expression !.source).toEqual('2');
+ expect(bindings[0].value !.source).toEqual('1');
+ expect(bindings[1].value !.source).toEqual('2');
});
it('should store the passed-in location', () => {
const bindings = parseTemplateBindings('a', '1,b 2', 'location');
- expect(bindings[0].expression !.location).toEqual('location');
+ expect(bindings[0].value !.location).toEqual('location');
+ });
+
+ it('should support common usage of ngIf', () => {
+ const bindings = parseTemplateBindings('ngIf', 'cond | pipe as foo, let x; ngIf as y');
+ expect(humanize(bindings)).toEqual([
+ // [ key, value, keyIsVar ]
+ ['ngIf', 'cond | pipe ', false],
+ ['foo', 'ngIf', true],
+ ['x', '$implicit', true],
+ ['y', 'ngIf', true],
+ ]);
+ });
+
+ it('should support common usage of ngFor', () => {
+ let bindings: TemplateBinding[];
+ bindings = parseTemplateBindings(
+ 'ngFor', 'let item; of items | slice:0:1 as collection, trackBy: func; index as i');
+ expect(humanize(bindings)).toEqual([
+ // [ key, value, keyIsVar ]
+ ['ngFor', null, false],
+ ['item', '$implicit', true],
+ ['ngForOf', 'items | slice:0:1 ', false],
+ ['collection', 'ngForOf', true],
+ ['ngForTrackBy', 'func', false],
+ ['i', 'index', true],
+ ]);
+
+ bindings = parseTemplateBindings(
+ 'ngFor', 'let item, of: [1,2,3] | pipe as items; let i=index, count as len');
+ expect(humanize(bindings)).toEqual([
+ // [ key, value, keyIsVar ]
+ ['ngFor', null, false],
+ ['item', '$implicit', true],
+ ['ngForOf', '[1,2,3] | pipe ', false],
+ ['items', 'ngForOf', true],
+ ['i', 'index', true],
+ ['len', 'count', true],
+ ]);
});
it('should support let notation', () => {
@@ -369,7 +416,7 @@ describe('parser', () => {
it('should parse pipes', () => {
const bindings = parseTemplateBindings('key', 'value|pipe');
- const ast = bindings[0].expression !.ast;
+ const ast = bindings[0].value !.ast;
expect(ast).toBeAnInstanceOf(BindingPipe);
});
diff --git a/packages/core/schematics/migrations/undecorated-classes-with-di/create_ngc_program.ts b/packages/core/schematics/migrations/undecorated-classes-with-di/create_ngc_program.ts
index 4c0f09cb5102..a25fc52c0bc0 100644
--- a/packages/core/schematics/migrations/undecorated-classes-with-di/create_ngc_program.ts
+++ b/packages/core/schematics/migrations/undecorated-classes-with-di/create_ngc_program.ts
@@ -20,6 +20,19 @@ export function createNgcProgram(
// NGC program. In order to ensure that the migration runs properly, we set "enableIvy" to false.
options.enableIvy = false;
+ // Libraries which have been generated with CLI versions past v6.2.0, explicitly set the
+ // flat-module options in their tsconfig files. This is problematic because by default,
+ // those tsconfig files do not specify explicit source files which can be considered as
+ // entry point for the flat-module bundle. Therefore the `@angular/compiler-cli` is unable
+ // to determine the flat module entry point and throws a compile error. This is not an issue
+ // for the libraries built with `ng-packagr`, because the tsconfig files are modified in-memory
+ // to specify an explicit flat module entry-point. Our migrations don't distinguish between
+ // libraries and applications, and also don't run `ng-packagr`. To ensure that such libraries
+ // can be successfully migrated, we remove the flat-module options to eliminate the flat module
+ // entry-point requirement. More context: https://github.com/angular/angular/issues/34985.
+ options.flatModuleId = undefined;
+ options.flatModuleOutFile = undefined;
+
const host = createHost(options);
// For this migration, we never need to read resources and can just return
diff --git a/packages/core/schematics/test/undecorated_classes_with_di_migration_spec.ts b/packages/core/schematics/test/undecorated_classes_with_di_migration_spec.ts
index b39a8ae74ad3..997dca5b5211 100644
--- a/packages/core/schematics/test/undecorated_classes_with_di_migration_spec.ts
+++ b/packages/core/schematics/test/undecorated_classes_with_di_migration_spec.ts
@@ -1473,6 +1473,40 @@ describe('Undecorated classes with DI migration', () => {
'TypeScript program failures');
});
+ // Regression test for: https://github.com/angular/angular/issues/34985.
+ it('should be able to migrate libraries with multiple source files and flat-module ' +
+ 'options set',
+ async() => {
+ writeFile('/tsconfig.json', JSON.stringify({
+ compilerOptions: {
+ lib: ['es2015'],
+ },
+ angularCompilerOptions:
+ {flatModuleId: 'AUTOGENERATED', flatModuleOutFile: 'AUTOGENERATED'}
+ }));
+
+ writeFile('/second.ts', ``);
+ writeFile('/test.ts', `
+ import {Injectable, NgModule, NgZone} from '@angular/core';
+
+ export class BaseClass {
+ constructor(zone: NgZone) {}
+ }
+
+ @Injectable({template: ''})
+ export class MyService extends BaseClass {}
+
+ @NgModule({providers: [MyService]})
+ export class AppModule {}
+ `);
+
+ await runMigration();
+
+ expect(errorOutput.length).toBe(0);
+ expect(warnOutput.length).toBe(0);
+ expect(tree.readContent('/test.ts')).toMatch(/@Injectable\(\)\nexport class BaseClass {/);
+ });
+
it('should not throw if resources could not be read', async() => {
writeFile('/index.ts', `
import {Component, NgModule} from '@angular/core';
diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts
index 26ad0dbf0a07..3b1215be9f44 100644
--- a/packages/core/src/render3/definition.ts
+++ b/packages/core/src/render3/definition.ts
@@ -289,56 +289,56 @@ export function ɵɵdefineComponent
(componentDefinition: {
*/
schemas?: SchemaMetadata[] | null;
}): never {
- // Initialize ngDevMode. This must be the first statement in ɵɵdefineComponent.
- // See the `initNgDevMode` docstring for more information.
- (typeof ngDevMode === 'undefined' || ngDevMode) && initNgDevMode();
-
- const type = componentDefinition.type;
- const typePrototype = type.prototype;
- const declaredInputs: {[key: string]: string} = {} as any;
- const def: Mutable, keyof ComponentDef> = {
- type: type,
- providersResolver: null,
- decls: componentDefinition.decls,
- vars: componentDefinition.vars,
- factory: null,
- template: componentDefinition.template || null !,
- consts: componentDefinition.consts || null,
- ngContentSelectors: componentDefinition.ngContentSelectors,
- hostBindings: componentDefinition.hostBindings || null,
- hostVars: componentDefinition.hostVars || 0,
- hostAttrs: componentDefinition.hostAttrs || null,
- contentQueries: componentDefinition.contentQueries || null,
- declaredInputs: declaredInputs,
- inputs: null !, // assigned in noSideEffects
- outputs: null !, // assigned in noSideEffects
- exportAs: componentDefinition.exportAs || null,
- onChanges: null,
- onInit: typePrototype.ngOnInit || null,
- doCheck: typePrototype.ngDoCheck || null,
- afterContentInit: typePrototype.ngAfterContentInit || null,
- afterContentChecked: typePrototype.ngAfterContentChecked || null,
- afterViewInit: typePrototype.ngAfterViewInit || null,
- afterViewChecked: typePrototype.ngAfterViewChecked || null,
- onDestroy: typePrototype.ngOnDestroy || null,
- onPush: componentDefinition.changeDetection === ChangeDetectionStrategy.OnPush,
- directiveDefs: null !, // assigned in noSideEffects
- pipeDefs: null !, // assigned in noSideEffects
- selectors: componentDefinition.selectors || EMPTY_ARRAY,
- viewQuery: componentDefinition.viewQuery || null,
- features: componentDefinition.features as DirectiveDefFeature[] || null,
- data: componentDefinition.data || {},
- // TODO(misko): convert ViewEncapsulation into const enum so that it can be used directly in the
- // next line. Also `None` should be 0 not 2.
- encapsulation: componentDefinition.encapsulation || ViewEncapsulation.Emulated,
- id: 'c',
- styles: componentDefinition.styles || EMPTY_ARRAY,
- _: null as never,
- setInput: null,
- schemas: componentDefinition.schemas || null,
- tView: null,
- };
- def._ = noSideEffects(() => {
+ return noSideEffects(() => {
+ // Initialize ngDevMode. This must be the first statement in ɵɵdefineComponent.
+ // See the `initNgDevMode` docstring for more information.
+ (typeof ngDevMode === 'undefined' || ngDevMode) && initNgDevMode();
+
+ const type = componentDefinition.type;
+ const typePrototype = type.prototype;
+ const declaredInputs: {[key: string]: string} = {} as any;
+ const def: Mutable, keyof ComponentDef> = {
+ type: type,
+ providersResolver: null,
+ decls: componentDefinition.decls,
+ vars: componentDefinition.vars,
+ factory: null,
+ template: componentDefinition.template || null !,
+ consts: componentDefinition.consts || null,
+ ngContentSelectors: componentDefinition.ngContentSelectors,
+ hostBindings: componentDefinition.hostBindings || null,
+ hostVars: componentDefinition.hostVars || 0,
+ hostAttrs: componentDefinition.hostAttrs || null,
+ contentQueries: componentDefinition.contentQueries || null,
+ declaredInputs: declaredInputs,
+ inputs: null !, // assigned in noSideEffects
+ outputs: null !, // assigned in noSideEffects
+ exportAs: componentDefinition.exportAs || null,
+ onChanges: null,
+ onInit: typePrototype.ngOnInit || null,
+ doCheck: typePrototype.ngDoCheck || null,
+ afterContentInit: typePrototype.ngAfterContentInit || null,
+ afterContentChecked: typePrototype.ngAfterContentChecked || null,
+ afterViewInit: typePrototype.ngAfterViewInit || null,
+ afterViewChecked: typePrototype.ngAfterViewChecked || null,
+ onDestroy: typePrototype.ngOnDestroy || null,
+ onPush: componentDefinition.changeDetection === ChangeDetectionStrategy.OnPush,
+ directiveDefs: null !, // assigned in noSideEffects
+ pipeDefs: null !, // assigned in noSideEffects
+ selectors: componentDefinition.selectors || EMPTY_ARRAY,
+ viewQuery: componentDefinition.viewQuery || null,
+ features: componentDefinition.features as DirectiveDefFeature[] || null,
+ data: componentDefinition.data || {},
+ // TODO(misko): convert ViewEncapsulation into const enum so that it can be used directly in
+ // the next line. Also `None` should be 0 not 2.
+ encapsulation: componentDefinition.encapsulation || ViewEncapsulation.Emulated,
+ id: 'c',
+ styles: componentDefinition.styles || EMPTY_ARRAY,
+ _: null as never,
+ setInput: null,
+ schemas: componentDefinition.schemas || null,
+ tView: null,
+ };
const directiveTypes = componentDefinition.directives !;
const feature = componentDefinition.features;
const pipeTypes = componentDefinition.pipes !;
@@ -353,9 +353,9 @@ export function ɵɵdefineComponent(componentDefinition: {
def.pipeDefs = pipeTypes ?
() => (typeof pipeTypes === 'function' ? pipeTypes() : pipeTypes).map(extractPipeDef) :
null;
- }) as never;
- return def as never;
+ return def as never;
+ });
}
/**
diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts
index 72dd5d85c8fa..c70505a2d1e6 100644
--- a/packages/core/src/render3/di.ts
+++ b/packages/core/src/render3/di.ts
@@ -14,6 +14,7 @@ import {getInjectorDef} from '../di/interface/defs';
import {InjectFlags} from '../di/interface/injector';
import {Type} from '../interface/type';
import {assertDefined, assertEqual} from '../util/assert';
+import {noSideEffects} from '../util/closure';
import {assertDirectiveDef} from './assert';
import {getFactoryDef} from './definition';
@@ -655,15 +656,17 @@ export function ɵɵgetFactoryOf(type: Type): FactoryFn|null {
* @codeGenApi
*/
export function ɵɵgetInheritedFactory(type: Type): (type: Type) => T {
- const proto = Object.getPrototypeOf(type.prototype).constructor as Type;
- const factory = (proto as any)[NG_FACTORY_DEF] || ɵɵgetFactoryOf(proto);
- if (factory !== null) {
- return factory;
- } else {
- // There is no factory defined. Either this was improper usage of inheritance
- // (no Angular decorator on the superclass) or there is no constructor at all
- // in the inheritance chain. Since the two cases cannot be distinguished, the
- // latter has to be assumed.
- return (t) => new t();
- }
+ return noSideEffects(() => {
+ const proto = Object.getPrototypeOf(type.prototype).constructor as Type;
+ const factory = (proto as any)[NG_FACTORY_DEF] || ɵɵgetFactoryOf(proto);
+ if (factory !== null) {
+ return factory;
+ } else {
+ // There is no factory defined. Either this was improper usage of inheritance
+ // (no Angular decorator on the superclass) or there is no constructor at all
+ // in the inheritance chain. Since the two cases cannot be distinguished, the
+ // latter has to be assumed.
+ return (t) => new t();
+ }
+ });
}
diff --git a/packages/core/src/render3/features/ng_onchanges_feature.ts b/packages/core/src/render3/features/ng_onchanges_feature.ts
index efc69de2ffd4..7d749c624fdd 100644
--- a/packages/core/src/render3/features/ng_onchanges_feature.ts
+++ b/packages/core/src/render3/features/ng_onchanges_feature.ts
@@ -35,26 +35,26 @@ type OnChangesExpando = OnChanges & {
* static ɵcmp = defineComponent({
* ...
* inputs: {name: 'publicName'},
- * features: [NgOnChangesFeature()]
+ * features: [NgOnChangesFeature]
* });
* ```
*
* @codeGenApi
*/
-export function ɵɵNgOnChangesFeature(): DirectiveDefFeature {
- // This option ensures that the ngOnChanges lifecycle hook will be inherited
- // from superclasses (in InheritDefinitionFeature).
- (NgOnChangesFeatureImpl as DirectiveDefFeature).ngInherit = true;
- return NgOnChangesFeatureImpl;
-}
-function NgOnChangesFeatureImpl(definition: DirectiveDef): void {
+export function ɵɵNgOnChangesFeature(definition: DirectiveDef): void {
if (definition.type.prototype.ngOnChanges) {
definition.setInput = ngOnChangesSetInput;
(definition as{onChanges: Function}).onChanges = wrapOnChanges();
}
}
+// This option ensures that the ngOnChanges lifecycle hook will be inherited
+// from superclasses (in InheritDefinitionFeature).
+/** @nocollapse */
+// tslint:disable-next-line:no-toplevel-property-access
+(ɵɵNgOnChangesFeature as DirectiveDefFeature).ngInherit = true;
+
function wrapOnChanges() {
return function wrapOnChangesHook_inPreviousChangesStorage(this: OnChanges) {
const simpleChangesStore = getSimpleChangesStore(this);
diff --git a/packages/core/src/render3/jit/module.ts b/packages/core/src/render3/jit/module.ts
index d0a4a194ace7..49b93eefd19f 100644
--- a/packages/core/src/render3/jit/module.ts
+++ b/packages/core/src/render3/jit/module.ts
@@ -455,19 +455,6 @@ export function transitiveScopesFor(moduleType: Type): NgModuleTransitiveS
},
};
- maybeUnwrapFn(def.declarations).forEach(declared => {
- const declaredWithDefs = declared as Type& { ɵpipe?: any; };
-
- if (getPipeDef(declaredWithDefs)) {
- scopes.compilation.pipes.add(declared);
- } else {
- // Either declared has a ɵcmp or ɵdir, or it's a component which hasn't
- // had its template compiled yet. In either case, it gets added to the compilation's
- // directives.
- scopes.compilation.directives.add(declared);
- }
- });
-
maybeUnwrapFn(def.imports).forEach((imported: Type) => {
const importedType = imported as Type& {
// If imported is an @NgModule:
@@ -485,6 +472,19 @@ export function transitiveScopesFor(moduleType: Type): NgModuleTransitiveS
importedScope.exported.pipes.forEach(entry => scopes.compilation.pipes.add(entry));
});
+ maybeUnwrapFn(def.declarations).forEach(declared => {
+ const declaredWithDefs = declared as Type& { ɵpipe?: any; };
+
+ if (getPipeDef(declaredWithDefs)) {
+ scopes.compilation.pipes.add(declared);
+ } else {
+ // Either declared has a ɵcmp or ɵdir, or it's a component which hasn't
+ // had its template compiled yet. In either case, it gets added to the compilation's
+ // directives.
+ scopes.compilation.directives.add(declared);
+ }
+ });
+
maybeUnwrapFn(def.exports).forEach((exported: Type) => {
const exportedType = exported as Type& {
// Components, Directives, NgModules, and Pipes can all be exported.
diff --git a/packages/core/src/util/closure.ts b/packages/core/src/util/closure.ts
index de36d31b0d45..19368bc807d2 100644
--- a/packages/core/src/util/closure.ts
+++ b/packages/core/src/util/closure.ts
@@ -15,6 +15,6 @@
* to something which is retained otherwise the call to `noSideEffects` will be removed by closure
* compiler.
*/
-export function noSideEffects(fn: () => void): string {
- return '' + {toString: fn};
-}
\ No newline at end of file
+export function noSideEffects(fn: () => T): T {
+ return {toString: fn}.toString() as unknown as T;
+}
diff --git a/packages/core/src/util/decorators.ts b/packages/core/src/util/decorators.ts
index c6cb35b7f42c..7ffb8fe56539 100644
--- a/packages/core/src/util/decorators.ts
+++ b/packages/core/src/util/decorators.ts
@@ -8,6 +8,10 @@
import {Type} from '../interface/type';
+import {noSideEffects} from './closure';
+
+
+
/**
* An interface implemented by all Angular type decorators, which allows them to be used as
* decorators as well as Angular syntax.
@@ -44,39 +48,41 @@ export function makeDecorator(
additionalProcessing?: (type: Type) => void,
typeFn?: (type: Type, ...args: any[]) => void):
{new (...args: any[]): any; (...args: any[]): any; (...args: any[]): (cls: any) => any;} {
- const metaCtor = makeMetadataCtor(props);
-
- function DecoratorFactory(
- this: unknown | typeof DecoratorFactory, ...args: any[]): (cls: Type) => any {
- if (this instanceof DecoratorFactory) {
- metaCtor.call(this, ...args);
- return this as typeof DecoratorFactory;
- }
+ return noSideEffects(() => {
+ const metaCtor = makeMetadataCtor(props);
+
+ function DecoratorFactory(
+ this: unknown | typeof DecoratorFactory, ...args: any[]): (cls: Type) => any {
+ if (this instanceof DecoratorFactory) {
+ metaCtor.call(this, ...args);
+ return this as typeof DecoratorFactory;
+ }
- const annotationInstance = new (DecoratorFactory as any)(...args);
- return function TypeDecorator(cls: Type) {
- if (typeFn) typeFn(cls, ...args);
- // Use of Object.defineProperty is important since it creates non-enumerable property which
- // prevents the property is copied during subclassing.
- const annotations = cls.hasOwnProperty(ANNOTATIONS) ?
- (cls as any)[ANNOTATIONS] :
- Object.defineProperty(cls, ANNOTATIONS, {value: []})[ANNOTATIONS];
- annotations.push(annotationInstance);
+ const annotationInstance = new (DecoratorFactory as any)(...args);
+ return function TypeDecorator(cls: Type) {
+ if (typeFn) typeFn(cls, ...args);
+ // Use of Object.defineProperty is important since it creates non-enumerable property which
+ // prevents the property is copied during subclassing.
+ const annotations = cls.hasOwnProperty(ANNOTATIONS) ?
+ (cls as any)[ANNOTATIONS] :
+ Object.defineProperty(cls, ANNOTATIONS, {value: []})[ANNOTATIONS];
+ annotations.push(annotationInstance);
- if (additionalProcessing) additionalProcessing(cls);
+ if (additionalProcessing) additionalProcessing(cls);
- return cls;
- };
- }
+ return cls;
+ };
+ }
- if (parentClass) {
- DecoratorFactory.prototype = Object.create(parentClass.prototype);
- }
+ if (parentClass) {
+ DecoratorFactory.prototype = Object.create(parentClass.prototype);
+ }
- DecoratorFactory.prototype.ngMetadataName = name;
- (DecoratorFactory as any).annotationCls = DecoratorFactory;
- return DecoratorFactory as any;
+ DecoratorFactory.prototype.ngMetadataName = name;
+ (DecoratorFactory as any).annotationCls = DecoratorFactory;
+ return DecoratorFactory as any;
+ });
}
function makeMetadataCtor(props?: (...args: any[]) => any): any {
@@ -92,77 +98,82 @@ function makeMetadataCtor(props?: (...args: any[]) => any): any {
export function makeParamDecorator(
name: string, props?: (...args: any[]) => any, parentClass?: any): any {
- const metaCtor = makeMetadataCtor(props);
- function ParamDecoratorFactory(
- this: unknown | typeof ParamDecoratorFactory, ...args: any[]): any {
- if (this instanceof ParamDecoratorFactory) {
- metaCtor.apply(this, args);
- return this;
- }
- const annotationInstance = new (ParamDecoratorFactory)(...args);
-
- (ParamDecorator).annotation = annotationInstance;
- return ParamDecorator;
-
- function ParamDecorator(cls: any, unusedKey: any, index: number): any {
- // Use of Object.defineProperty is important since it creates non-enumerable property which
- // prevents the property is copied during subclassing.
- const parameters = cls.hasOwnProperty(PARAMETERS) ?
- (cls as any)[PARAMETERS] :
- Object.defineProperty(cls, PARAMETERS, {value: []})[PARAMETERS];
-
- // there might be gaps if some in between parameters do not have annotations.
- // we pad with nulls.
- while (parameters.length <= index) {
- parameters.push(null);
+ return noSideEffects(() => {
+ const metaCtor = makeMetadataCtor(props);
+ function ParamDecoratorFactory(
+ this: unknown | typeof ParamDecoratorFactory, ...args: any[]): any {
+ if (this instanceof ParamDecoratorFactory) {
+ metaCtor.apply(this, args);
+ return this;
+ }
+ const annotationInstance = new (ParamDecoratorFactory)(...args);
+
+ (ParamDecorator).annotation = annotationInstance;
+ return ParamDecorator;
+
+ function ParamDecorator(cls: any, unusedKey: any, index: number): any {
+ // Use of Object.defineProperty is important since it creates non-enumerable property which
+ // prevents the property is copied during subclassing.
+ const parameters = cls.hasOwnProperty(PARAMETERS) ?
+ (cls as any)[PARAMETERS] :
+ Object.defineProperty(cls, PARAMETERS, {value: []})[PARAMETERS];
+
+ // there might be gaps if some in between parameters do not have annotations.
+ // we pad with nulls.
+ while (parameters.length <= index) {
+ parameters.push(null);
+ }
+
+ (parameters[index] = parameters[index] || []).push(annotationInstance);
+ return cls;
}
-
- (parameters[index] = parameters[index] || []).push(annotationInstance);
- return cls;
}
- }
- if (parentClass) {
- ParamDecoratorFactory.prototype = Object.create(parentClass.prototype);
- }
- ParamDecoratorFactory.prototype.ngMetadataName = name;
- (ParamDecoratorFactory).annotationCls = ParamDecoratorFactory;
- return ParamDecoratorFactory;
+ if (parentClass) {
+ ParamDecoratorFactory.prototype = Object.create(parentClass.prototype);
+ }
+ ParamDecoratorFactory.prototype.ngMetadataName = name;
+ (ParamDecoratorFactory).annotationCls = ParamDecoratorFactory;
+ return ParamDecoratorFactory;
+ });
}
export function makePropDecorator(
name: string, props?: (...args: any[]) => any, parentClass?: any,
additionalProcessing?: (target: any, name: string, ...args: any[]) => void): any {
- const metaCtor = makeMetadataCtor(props);
+ return noSideEffects(() => {
+ const metaCtor = makeMetadataCtor(props);
+
+ function PropDecoratorFactory(
+ this: unknown | typeof PropDecoratorFactory, ...args: any[]): any {
+ if (this instanceof PropDecoratorFactory) {
+ metaCtor.apply(this, args);
+ return this;
+ }
- function PropDecoratorFactory(this: unknown | typeof PropDecoratorFactory, ...args: any[]): any {
- if (this instanceof PropDecoratorFactory) {
- metaCtor.apply(this, args);
- return this;
- }
+ const decoratorInstance = new (PropDecoratorFactory)(...args);
- const decoratorInstance = new (PropDecoratorFactory)(...args);
+ function PropDecorator(target: any, name: string) {
+ const constructor = target.constructor;
+ // Use of Object.defineProperty is important since it creates non-enumerable property which
+ // prevents the property is copied during subclassing.
+ const meta = constructor.hasOwnProperty(PROP_METADATA) ?
+ (constructor as any)[PROP_METADATA] :
+ Object.defineProperty(constructor, PROP_METADATA, {value: {}})[PROP_METADATA];
+ meta[name] = meta.hasOwnProperty(name) && meta[name] || [];
+ meta[name].unshift(decoratorInstance);
- function PropDecorator(target: any, name: string) {
- const constructor = target.constructor;
- // Use of Object.defineProperty is important since it creates non-enumerable property which
- // prevents the property is copied during subclassing.
- const meta = constructor.hasOwnProperty(PROP_METADATA) ?
- (constructor as any)[PROP_METADATA] :
- Object.defineProperty(constructor, PROP_METADATA, {value: {}})[PROP_METADATA];
- meta[name] = meta.hasOwnProperty(name) && meta[name] || [];
- meta[name].unshift(decoratorInstance);
+ if (additionalProcessing) additionalProcessing(target, name, ...args);
+ }
- if (additionalProcessing) additionalProcessing(target, name, ...args);
+ return PropDecorator;
}
- return PropDecorator;
- }
-
- if (parentClass) {
- PropDecoratorFactory.prototype = Object.create(parentClass.prototype);
- }
+ if (parentClass) {
+ PropDecoratorFactory.prototype = Object.create(parentClass.prototype);
+ }
- PropDecoratorFactory.prototype.ngMetadataName = name;
- (PropDecoratorFactory).annotationCls = PropDecoratorFactory;
- return PropDecoratorFactory;
+ PropDecoratorFactory.prototype.ngMetadataName = name;
+ (PropDecoratorFactory).annotationCls = PropDecoratorFactory;
+ return PropDecoratorFactory;
+ });
}
diff --git a/packages/core/test/acceptance/directive_spec.ts b/packages/core/test/acceptance/directive_spec.ts
index 41ba900960ef..2903f56d1a5d 100644
--- a/packages/core/test/acceptance/directive_spec.ts
+++ b/packages/core/test/acceptance/directive_spec.ts
@@ -7,7 +7,7 @@
*/
import {CommonModule} from '@angular/common';
-import {Component, Directive, ElementRef, EventEmitter, Output, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
+import {Component, Directive, ElementRef, EventEmitter, NgModule, Output, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
import {Input} from '@angular/core/src/metadata';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
@@ -633,4 +633,96 @@ describe('directives', () => {
expect(div.getAttribute('title')).toBe('a');
});
});
+
+ describe('directives with the same selector', () => {
+ it('should process Directives from `declarations` list after imported ones', () => {
+ const log: string[] = [];
+ @Directive({selector: '[dir]'})
+ class DirectiveA {
+ constructor() { log.push('DirectiveA.constructor'); }
+ ngOnInit() { log.push('DirectiveA.ngOnInit'); }
+ }
+
+ @NgModule({
+ declarations: [DirectiveA],
+ exports: [DirectiveA],
+ })
+ class ModuleA {
+ }
+
+ @Directive({selector: '[dir]'})
+ class DirectiveB {
+ constructor() { log.push('DirectiveB.constructor'); }
+ ngOnInit() { log.push('DirectiveB.ngOnInit'); }
+ }
+
+ @Component({
+ selector: 'app',
+ template: '
',
+ })
+ class App {
+ }
+
+ TestBed.configureTestingModule({
+ imports: [ModuleA],
+ declarations: [DirectiveB, App],
+ });
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+
+ expect(log).toEqual([
+ 'DirectiveA.constructor', 'DirectiveB.constructor', 'DirectiveA.ngOnInit',
+ 'DirectiveB.ngOnInit'
+ ]);
+ });
+
+ it('should respect imported module order', () => {
+ const log: string[] = [];
+ @Directive({selector: '[dir]'})
+ class DirectiveA {
+ constructor() { log.push('DirectiveA.constructor'); }
+ ngOnInit() { log.push('DirectiveA.ngOnInit'); }
+ }
+
+ @NgModule({
+ declarations: [DirectiveA],
+ exports: [DirectiveA],
+ })
+ class ModuleA {
+ }
+
+ @Directive({selector: '[dir]'})
+ class DirectiveB {
+ constructor() { log.push('DirectiveB.constructor'); }
+ ngOnInit() { log.push('DirectiveB.ngOnInit'); }
+ }
+
+ @NgModule({
+ declarations: [DirectiveB],
+ exports: [DirectiveB],
+ })
+ class ModuleB {
+ }
+
+ @Component({
+ selector: 'app',
+ template: '
',
+ })
+ class App {
+ }
+
+ TestBed.configureTestingModule({
+ imports: [ModuleA, ModuleB],
+ declarations: [App],
+ });
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+
+ expect(log).toEqual([
+ 'DirectiveA.constructor', 'DirectiveB.constructor', 'DirectiveA.ngOnInit',
+ 'DirectiveB.ngOnInit'
+ ]);
+ });
+
+ });
});
diff --git a/packages/core/test/acceptance/pipe_spec.ts b/packages/core/test/acceptance/pipe_spec.ts
index 3dd1be88ebd1..da9a561d2b2c 100644
--- a/packages/core/test/acceptance/pipe_spec.ts
+++ b/packages/core/test/acceptance/pipe_spec.ts
@@ -110,6 +110,86 @@ describe('pipe', () => {
expect(fixture.nativeElement.textContent).toEqual('value a b default 0 1 2 3');
});
+ it('should pick a Pipe defined in `declarations` over imported Pipes', () => {
+ @Pipe({name: 'number'})
+ class PipeA implements PipeTransform {
+ transform(value: any) { return `PipeA: ${value}`; }
+ }
+
+ @NgModule({
+ declarations: [PipeA],
+ exports: [PipeA],
+ })
+ class ModuleA {
+ }
+
+ @Pipe({name: 'number'})
+ class PipeB implements PipeTransform {
+ transform(value: any) { return `PipeB: ${value}`; }
+ }
+
+ @Component({
+ selector: 'app',
+ template: '{{ count | number }}',
+ })
+ class App {
+ count = 10;
+ }
+
+ TestBed.configureTestingModule({
+ imports: [ModuleA],
+ declarations: [PipeB, App],
+ });
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.textContent).toBe('PipeB: 10');
+ });
+
+ it('should respect imported module order when selecting Pipe (last imported Pipe is used)',
+ () => {
+ @Pipe({name: 'number'})
+ class PipeA implements PipeTransform {
+ transform(value: any) { return `PipeA: ${value}`; }
+ }
+
+ @NgModule({
+ declarations: [PipeA],
+ exports: [PipeA],
+ })
+ class ModuleA {
+ }
+
+ @Pipe({name: 'number'})
+ class PipeB implements PipeTransform {
+ transform(value: any) { return `PipeB: ${value}`; }
+ }
+
+ @NgModule({
+ declarations: [PipeB],
+ exports: [PipeB],
+ })
+ class ModuleB {
+ }
+
+ @Component({
+ selector: 'app',
+ template: '{{ count | number }}',
+ })
+ class App {
+ count = 10;
+ }
+
+ TestBed.configureTestingModule({
+ imports: [ModuleA, ModuleB],
+ declarations: [App],
+ });
+ const fixture = TestBed.createComponent(App);
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.textContent).toBe('PipeB: 10');
+ });
+
it('should do nothing when no change', () => {
let calls: any[] = [];
diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
index facce6d3a75f..7470bbe21eb9 100644
--- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json
@@ -167,6 +167,9 @@
{
"name": "attachPatchData"
},
+ {
+ "name": "autoRegisterModuleById"
+ },
{
"name": "baseResolveDirective"
},
diff --git a/packages/core/test/bundling/injection/bundle.golden_symbols.json b/packages/core/test/bundling/injection/bundle.golden_symbols.json
index fee0ea7c7f84..125db87f31bb 100644
--- a/packages/core/test/bundling/injection/bundle.golden_symbols.json
+++ b/packages/core/test/bundling/injection/bundle.golden_symbols.json
@@ -223,5 +223,8 @@
},
{
"name": "ɵɵinject"
+ },
+ {
+ "name": "noSideEffects"
}
]
diff --git a/packages/core/test/render3/common_with_def.ts b/packages/core/test/render3/common_with_def.ts
index 9a40985fcdd7..96904c1b8e5c 100644
--- a/packages/core/test/render3/common_with_def.ts
+++ b/packages/core/test/render3/common_with_def.ts
@@ -41,7 +41,7 @@ NgIf.ɵfac = () =>
NgTemplateOutlet.ɵdir = ɵɵdefineDirective({
type: NgTemplateOutletDef,
selectors: [['', 'ngTemplateOutlet', '']],
- features: [ɵɵNgOnChangesFeature()],
+ features: [ɵɵNgOnChangesFeature],
inputs:
{ngTemplateOutlet: 'ngTemplateOutlet', ngTemplateOutletContext: 'ngTemplateOutletContext'}
});
diff --git a/packages/elements/test/BUILD.bazel b/packages/elements/test/BUILD.bazel
index 7e343c8722bb..c73f50e5bc1c 100644
--- a/packages/elements/test/BUILD.bazel
+++ b/packages/elements/test/BUILD.bazel
@@ -30,6 +30,7 @@ filegroup(
testonly = True,
# do not sort
srcs = [
+ "@npm//:node_modules/core-js/client/core.js",
"@npm//:node_modules/@webcomponents/custom-elements/src/native-shim.js",
"@npm//:node_modules/reflect-metadata/Reflect.js",
"//packages/zone.js/dist:zone.js",
@@ -42,11 +43,6 @@ karma_web_test_suite(
bootstrap = [
":elements_test_bootstrap_scripts",
],
- tags = [
- # FIXME: timed out in CI
- "fixme-saucelabs-ivy",
- "fixme-saucelabs-ve",
- ],
deps = [
":test_lib",
],
diff --git a/packages/language-service/src/completions.ts b/packages/language-service/src/completions.ts
index 64abe85f5b80..becfae278067 100644
--- a/packages/language-service/src/completions.ts
+++ b/packages/language-service/src/completions.ts
@@ -567,8 +567,8 @@ class ExpressionVisitor extends NullTemplateVisitor {
}
}
- if (binding.expression && inSpan(valueRelativePosition, binding.expression.ast.span)) {
- this.processExpressionCompletions(binding.expression.ast);
+ if (binding.value && inSpan(valueRelativePosition, binding.value.ast.span)) {
+ this.processExpressionCompletions(binding.value.ast);
return;
}
diff --git a/packages/language-service/src/expression_diagnostics.ts b/packages/language-service/src/expression_diagnostics.ts
index 39d5e3405ccd..2d060ee894e7 100644
--- a/packages/language-service/src/expression_diagnostics.ts
+++ b/packages/language-service/src/expression_diagnostics.ts
@@ -91,10 +91,8 @@ function getVarDeclarations(
continue;
}
for (const variable of current.variables) {
- let symbol = info.members.get(variable.value);
- if (!symbol) {
- symbol = getVariableTypeFromDirectiveContext(variable.value, info.query, current);
- }
+ let symbol = getVariableTypeFromDirectiveContext(variable.value, info.query, current);
+
const kind = info.query.getTypeKind(symbol);
if (kind === BuiltinType.Any || kind === BuiltinType.Unbound) {
// For special cases such as ngFor and ngIf, the any type is not very useful.
diff --git a/packages/language-service/src/locate_symbol.ts b/packages/language-service/src/locate_symbol.ts
index b4967a141543..57f4b4bae2f7 100644
--- a/packages/language-service/src/locate_symbol.ts
+++ b/packages/language-service/src/locate_symbol.ts
@@ -209,10 +209,10 @@ function getSymbolInMicrosyntax(info: AstResult, path: TemplateAstPath, attribut
// Find the symbol that contains the position.
templateBindings.filter(tb => !tb.keyIsVar).forEach(tb => {
- if (inSpan(valueRelativePosition, tb.expression?.ast.span)) {
+ if (inSpan(valueRelativePosition, tb.value?.ast.span)) {
const dinfo = diagnosticInfoFromTemplateInfo(info);
const scope = getExpressionScope(dinfo, path);
- result = getExpressionSymbol(scope, tb.expression !, path.position, info.template.query);
+ result = getExpressionSymbol(scope, tb.value !, path.position, info.template.query);
} else if (inSpan(valueRelativePosition, tb.span)) {
const template = path.first(EmbeddedTemplateAst);
if (template) {
diff --git a/packages/language-service/test/project/app/parsing-cases.ts b/packages/language-service/test/project/app/parsing-cases.ts
index 988aa6c5a451..e31f6a6e9b43 100644
--- a/packages/language-service/test/project/app/parsing-cases.ts
+++ b/packages/language-service/test/project/app/parsing-cases.ts
@@ -170,6 +170,8 @@ export class TemplateReference {
primitiveIndexType: {[name: string]: string} = {};
anyValue: any;
optional?: string;
+ // Use to test the `index` variable conflict between the `ngFor` and component context.
+ index = null;
myClick(event: any) {}
}
diff --git a/packages/localize/src/tools/src/diagnostics.ts b/packages/localize/src/tools/src/diagnostics.ts
index 5ba8a52862db..1be6a7cccbf0 100644
--- a/packages/localize/src/tools/src/diagnostics.ts
+++ b/packages/localize/src/tools/src/diagnostics.ts
@@ -15,4 +15,15 @@ export class Diagnostics {
get hasErrors() { return this.messages.some(m => m.type === 'error'); }
warn(message: string) { this.messages.push({type: 'warning', message}); }
error(message: string) { this.messages.push({type: 'error', message}); }
+ formatDiagnostics(message: string): string {
+ const errors = this.messages !.filter(d => d.type === 'error').map(d => ' - ' + d.message);
+ const warnings = this.messages !.filter(d => d.type === 'warning').map(d => ' - ' + d.message);
+ if (errors.length) {
+ message += '\nERRORS:\n' + errors.join('\n');
+ }
+ if (warnings.length) {
+ message += '\nWARNINGS:\n' + warnings.join('\n');
+ }
+ return message;
+ }
}
diff --git a/packages/localize/src/tools/src/translate/main.ts b/packages/localize/src/tools/src/translate/main.ts
index 3c2c04233d63..bb683f62b61d 100644
--- a/packages/localize/src/tools/src/translate/main.ts
+++ b/packages/localize/src/tools/src/translate/main.ts
@@ -142,7 +142,7 @@ export function translateFiles({sourceRootPath, sourceFilePaths, translationFile
[
new Xliff2TranslationParser(),
new Xliff1TranslationParser(),
- new XtbTranslationParser(diagnostics),
+ new XtbTranslationParser(),
new SimpleJsonTranslationParser(),
],
diagnostics);
diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts b/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts
index 33096987b111..8c37a985faab 100644
--- a/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts
+++ b/packages/localize/src/tools/src/translate/translation_files/translation_loader.ts
@@ -14,7 +14,9 @@ import {TranslationParser} from './translation_parsers/translation_parser';
* Use this class to load a collection of translation files from disk.
*/
export class TranslationLoader {
- constructor(private translationParsers: TranslationParser[], private diagnostics: Diagnostics) {}
+ constructor(
+ private translationParsers: TranslationParser[],
+ /** @deprecated */ private diagnostics?: Diagnostics) {}
/**
* Load and parse the translation files into a collection of `TranslationBundles`.
@@ -34,22 +36,37 @@ export class TranslationLoader {
return translationFilePaths.map((filePath, index) => {
const fileContents = FileUtils.readFile(filePath);
for (const translationParser of this.translationParsers) {
- if (translationParser.canParse(filePath, fileContents)) {
- const providedLocale = translationFileLocales[index];
- const {locale: parsedLocale, translations} =
- translationParser.parse(filePath, fileContents);
- const locale = providedLocale || parsedLocale;
- if (locale === undefined) {
- throw new Error(
- `The translation file "${filePath}" does not contain a target locale and no explicit locale was provided for this file.`);
- }
- if (parsedLocale !== undefined && providedLocale !== undefined &&
- parsedLocale !== providedLocale) {
- this.diagnostics.warn(
- `The provided locale "${providedLocale}" does not match the target locale "${parsedLocale}" found in the translation file "${filePath}".`);
- }
- return {locale, translations};
+ const result = translationParser.canParse(filePath, fileContents);
+ if (!result) {
+ continue;
}
+
+ const {locale: parsedLocale, translations, diagnostics} =
+ translationParser.parse(filePath, fileContents, result);
+ if (diagnostics.hasErrors) {
+ throw new Error(diagnostics.formatDiagnostics(
+ `The translation file "${filePath}" could not be parsed.`));
+ }
+
+ const providedLocale = translationFileLocales[index];
+ const locale = providedLocale || parsedLocale;
+ if (locale === undefined) {
+ throw new Error(
+ `The translation file "${filePath}" does not contain a target locale and no explicit locale was provided for this file.`);
+ }
+
+ if (parsedLocale !== undefined && providedLocale !== undefined &&
+ parsedLocale !== providedLocale) {
+ diagnostics.warn(
+ `The provided locale "${providedLocale}" does not match the target locale "${parsedLocale}" found in the translation file "${filePath}".`);
+ }
+
+ // If we were passed a diagnostics object then copy the messages over to it.
+ if (this.diagnostics) {
+ this.diagnostics.messages.push(...diagnostics.messages);
+ }
+
+ return {locale, translations, diagnostics};
}
throw new Error(
`There is no "TranslationParser" that can parse this translation file: ${filePath}.`);
diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser.ts
index 0421529e96e1..e21299215d84 100644
--- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser.ts
+++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/simple_json_translation_parser.ts
@@ -7,6 +7,7 @@
*/
import {ɵMessageId, ɵParsedTranslation, ɵparseTranslation} from '@angular/localize';
import {extname} from 'path';
+import {Diagnostics} from '../../../diagnostics';
import {ParsedTranslationBundle, TranslationParser} from './translation_parser';
/**
@@ -32,6 +33,6 @@ export class SimpleJsonTranslationParser implements TranslationParser {
const targetMessage = translations[messageId];
parsedTranslations[messageId] = ɵparseTranslation(targetMessage);
}
- return {locale: parsedLocale, translations: parsedTranslations};
+ return {locale: parsedLocale, translations: parsedTranslations, diagnostics: new Diagnostics()};
}
}
diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_parser.ts
index 802e6dfd7d37..14b2ab71bcf7 100644
--- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_parser.ts
+++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_parser.ts
@@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ɵMessageId, ɵParsedTranslation} from '@angular/localize/private';
+import {Diagnostics} from '../../../diagnostics';
/**
* An object that holds translations that have been parsed from a translation file.
@@ -13,25 +14,62 @@ import {ɵMessageId, ɵParsedTranslation} from '@angular/localize/private';
export interface ParsedTranslationBundle {
locale: string|undefined;
translations: Record<ɵMessageId, ɵParsedTranslation>;
+ diagnostics: Diagnostics;
}
/**
* Implement this interface to provide a class that can parse the contents of a translation file.
+ *
+ * The `canParse()` method can return a hint that can be used by the `parse()` method to speed up
+ * parsing. This allows the parser to do significant work to determine if the file can be parsed
+ * without duplicating the work when it comes to actually parsing the file.
+ *
+ * Example usage:
+ *
+ * ```
+ * const parser: TranslationParser = getParser();
+ * const result = parser.canParse(filePath, content);
+ * if (result) {
+ * return parser.parse(filePath, content, result);
+ * }
+ * ```
*/
-export interface TranslationParser {
+export interface TranslationParser {
/**
- * Returns true if this parser can parse the given file.
+ * Can this parser parse the given file?
*
* @param filePath The absolute path to the translation file.
* @param contents The contents of the translation file.
+ * @returns A hint, which can be used in doing the actual parsing, if the file can be parsed by
+ * this parser; false otherwise.
*/
- canParse(filePath: string, contents: string): boolean;
+ canParse(filePath: string, contents: string): Hint|false;
/**
* Parses the given file, extracting the target locale and translations.
*
+ * Note that this method should not throw an error. Check the `bundle.diagnostics` property for
+ * potential parsing errors and warnings.
+ *
+ * @param filePath The absolute path to the translation file.
+ * @param contents The contents of the translation file.
+ * @param hint A value that can be used by the parser to speed up parsing of the file. This will
+ * have been provided as the return result from calling `canParse()`.
+ * @returns The translation bundle parsed from the file.
+ * @throws No errors. If there was a problem with parsing the bundle will contain errors
+ * in the `diagnostics` property.
+ */
+ parse(filePath: string, contents: string, hint: Hint): ParsedTranslationBundle;
+ /**
+ * Parses the given file, extracting the target locale and translations.
+ *
+ * @deprecated This overload is kept for backward compatibility. Going forward use the Hint
+ * returned from `canParse()` so that this method can avoid duplicating effort.
+ *
* @param filePath The absolute path to the translation file.
* @param contents The contents of the translation file.
+ * @returns The translation bundle parsed from the file.
+ * @throws An error if there was a problem parsing this file.
*/
parse(filePath: string, contents: string): ParsedTranslationBundle;
}
diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_utils.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_utils.ts
index 92111e458be1..731bfa30b1f5 100644
--- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_utils.ts
+++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/translation_utils.ts
@@ -5,7 +5,8 @@
* 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 {Element, LexerRange, Node, XmlParser} from '@angular/compiler';
+import {Element, LexerRange, Node, ParseError, ParseErrorLevel, ParseSourceSpan, XmlParser} from '@angular/compiler';
+import {Diagnostics} from '../../../diagnostics';
import {TranslationParseError} from './translation_parse_error';
export function getAttrOrThrow(element: Element, attrName: string): string {
@@ -22,6 +23,14 @@ export function getAttribute(element: Element, attrName: string): string|undefin
return attr !== undefined ? attr.value : undefined;
}
+/**
+ * Parse the "contents" of an XML element.
+ *
+ * This would be equivalent to parsing the `innerHTML` string of an HTML document.
+ *
+ * @param element The element whose inner range we want to parse.
+ * @returns a collection of XML `Node` objects that were parsed from the element's contents.
+ */
export function parseInnerRange(element: Element): Node[] {
const xmlParser = new XmlParser();
const xml = xmlParser.parse(
@@ -33,6 +42,10 @@ export function parseInnerRange(element: Element): Node[] {
return xml.rootNodes;
}
+/**
+ * Compute a `LexerRange` that contains all the children of the given `element`.
+ * @param element The element whose inner range we want to compute.
+ */
function getInnerRange(element: Element): LexerRange {
const start = element.startSourceSpan !.end;
const end = element.endSourceSpan !.start;
@@ -42,4 +55,94 @@ function getInnerRange(element: Element): LexerRange {
startCol: start.col,
endPos: end.offset,
};
-}
\ No newline at end of file
+}
+
+/**
+ * This "hint" object is used to pass information from `canParse()` to `parse()` for
+ * `TranslationParser`s that expect XML contents.
+ *
+ * This saves the `parse()` method from having to re-parse the XML.
+ */
+export interface XmlTranslationParserHint {
+ element: Element;
+ errors: ParseError[];
+}
+
+/**
+ * Can this XML be parsed for translations, given the expected `rootNodeName` and expected root node
+ * `attributes` that should appear in the file.
+ *
+ * @param filePath The path to the file being checked.
+ * @param contents The contents of the file being checked.
+ * @param rootNodeName The expected name of an XML root node that should exist.
+ * @param attributes The attributes (and their values) that should appear on the root node.
+ * @returns The `XmlTranslationParserHint` object for use by `TranslationParser.parse()` if the XML
+ * document has the expected format.
+ */
+export function canParseXml(
+ filePath: string, contents: string, rootNodeName: string,
+ attributes: Record): XmlTranslationParserHint|false {
+ const xmlParser = new XmlParser();
+ const xml = xmlParser.parse(contents, filePath);
+
+ if (xml.rootNodes.length === 0 ||
+ xml.errors.some(error => error.level === ParseErrorLevel.ERROR)) {
+ return false;
+ }
+
+ const rootElements = xml.rootNodes.filter(isNamedElement(rootNodeName));
+ const rootElement = rootElements[0];
+ if (rootElement === undefined) {
+ return false;
+ }
+
+ for (const attrKey of Object.keys(attributes)) {
+ const attr = rootElement.attrs.find(attr => attr.name === attrKey);
+ if (attr === undefined || attr.value !== attributes[attrKey]) {
+ return false;
+ }
+ }
+
+ if (rootElements.length > 1) {
+ xml.errors.push(new ParseError(
+ xml.rootNodes[1].sourceSpan,
+ 'Unexpected root node. XLIFF 1.2 files should only have a single root node.',
+ ParseErrorLevel.WARNING));
+ }
+
+ return {element: rootElement, errors: xml.errors};
+}
+
+/**
+ * Create a predicate, which can be used by things like `Array.filter()`, that will match a named
+ * XML Element from a collection of XML Nodes.
+ *
+ * @param name The expected name of the element to match.
+ */
+export function isNamedElement(name: string): (node: Node) => node is Element {
+ function predicate(node: Node): node is Element {
+ return node instanceof Element && node.name === name;
+ }
+ return predicate;
+}
+
+/**
+ * Add an XML parser related message to the given `diagnostics` object.
+ */
+export function addParseDiagnostic(
+ diagnostics: Diagnostics, sourceSpan: ParseSourceSpan, message: string,
+ level: ParseErrorLevel): void {
+ addParseError(diagnostics, new ParseError(sourceSpan, message, level));
+}
+
+/**
+ * Copy the formatted error message from the given `parseError` object into the given `diagnostics`
+ * object.
+ */
+export function addParseError(diagnostics: Diagnostics, parseError: ParseError): void {
+ if (parseError.level === ParseErrorLevel.ERROR) {
+ diagnostics.error(parseError.toString());
+ } else {
+ diagnostics.warn(parseError.toString());
+ }
+}
diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts
index 1301d5ecda9b..2656cc858490 100644
--- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts
+++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff1_translation_parser.ts
@@ -5,19 +5,16 @@
* 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 {Element, Node, XmlParser, visitAll} from '@angular/compiler';
-import {ɵMessageId, ɵParsedTranslation} from '@angular/localize';
-import {extname} from 'path';
+import {Element, ParseErrorLevel, visitAll} from '@angular/compiler';
+import {ɵParsedTranslation} from '@angular/localize';
+import {Diagnostics} from '../../../diagnostics';
import {BaseVisitor} from '../base_visitor';
import {MessageSerializer} from '../message_serialization/message_serializer';
import {TargetMessageRenderer} from '../message_serialization/target_message_renderer';
-import {TranslationParseError} from './translation_parse_error';
import {ParsedTranslationBundle, TranslationParser} from './translation_parser';
-import {getAttrOrThrow, getAttribute, parseInnerRange} from './translation_utils';
-
-const XLIFF_1_2_NS_REGEX = /xmlns="urn:oasis:names:tc:xliff:document:1.2"/;
+import {XmlTranslationParserHint, addParseDiagnostic, addParseError, canParseXml, getAttribute, isNamedElement, parseInnerRange} from './translation_utils';
/**
* A translation parser that can load XLIFF 1.2 files.
@@ -26,67 +23,132 @@ const XLIFF_1_2_NS_REGEX = /xmlns="urn:oasis:names:tc:xliff:document:1.2"/;
* http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2.html
*
*/
-export class Xliff1TranslationParser implements TranslationParser {
- canParse(filePath: string, contents: string): boolean {
- return (extname(filePath) === '.xlf') && XLIFF_1_2_NS_REGEX.test(contents);
+export class Xliff1TranslationParser implements TranslationParser {
+ canParse(filePath: string, contents: string): XmlTranslationParserHint|false {
+ return canParseXml(filePath, contents, 'xliff', {version: '1.2'});
+ }
+
+ parse(filePath: string, contents: string, hint?: XmlTranslationParserHint):
+ ParsedTranslationBundle {
+ if (hint) {
+ return this.extractBundle(hint);
+ } else {
+ return this.extractBundleDeprecated(filePath, contents);
+ }
+ }
+
+ private extractBundle({element, errors}: XmlTranslationParserHint): ParsedTranslationBundle {
+ const diagnostics = new Diagnostics();
+ errors.forEach(e => addParseError(diagnostics, e));
+
+ if (element.children.length === 0) {
+ addParseDiagnostic(
+ diagnostics, element.sourceSpan, 'Missing expected element',
+ ParseErrorLevel.WARNING);
+ return {locale: undefined, translations: {}, diagnostics};
+ }
+
+ const files = element.children.filter(isNamedElement('file'));
+ if (files.length === 0) {
+ addParseDiagnostic(
+ diagnostics, element.sourceSpan, 'No elements found in ',
+ ParseErrorLevel.WARNING);
+ } else if (files.length > 1) {
+ addParseDiagnostic(
+ diagnostics, files[1].sourceSpan, 'More than one element found in ',
+ ParseErrorLevel.WARNING);
+ }
+
+ const bundle: ParsedTranslationBundle = {locale: undefined, translations: {}, diagnostics};
+ const translationVisitor = new XliffTranslationVisitor();
+ const localesFound = new Set();
+ for (const file of files) {
+ const locale = getAttribute(file, 'target-language');
+ if (locale !== undefined) {
+ localesFound.add(locale);
+ bundle.locale = locale;
+ }
+ visitAll(translationVisitor, file.children, bundle);
+ }
+
+ if (localesFound.size > 1) {
+ addParseDiagnostic(
+ diagnostics, element.sourceSpan,
+ `More than one locale found in translation file: ${JSON.stringify(Array.from(localesFound))}. Using "${bundle.locale}"`,
+ ParseErrorLevel.WARNING);
+ }
+
+ return bundle;
}
- parse(filePath: string, contents: string): ParsedTranslationBundle {
- const xmlParser = new XmlParser();
- const xml = xmlParser.parse(contents, filePath);
- const bundle = XliffFileElementVisitor.extractBundle(xml.rootNodes);
- if (bundle === undefined) {
+ private extractBundleDeprecated(filePath: string, contents: string) {
+ const hint = this.canParse(filePath, contents);
+ if (!hint) {
throw new Error(`Unable to parse "${filePath}" as XLIFF 1.2 format.`);
}
+ const bundle = this.extractBundle(hint);
+ if (bundle.diagnostics.hasErrors) {
+ const message =
+ bundle.diagnostics.formatDiagnostics(`Failed to parse "${filePath}" as XLIFF 1.2 format`);
+ throw new Error(message);
+ }
return bundle;
}
}
class XliffFileElementVisitor extends BaseVisitor {
- private bundle: ParsedTranslationBundle|undefined;
-
- static extractBundle(xliff: Node[]): ParsedTranslationBundle|undefined {
- const visitor = new this();
- visitAll(visitor, xliff);
- return visitor.bundle;
+ visitElement(fileElement: Element): any {
+ if (fileElement.name === 'file') {
+ return {fileElement, locale: getAttribute(fileElement, 'target-language')};
+ }
}
+}
- visitElement(element: Element): any {
- if (element.name === 'file') {
- this.bundle = {
- locale: getAttribute(element, 'target-language'),
- translations: XliffTranslationVisitor.extractTranslations(element)
- };
+class XliffTranslationVisitor extends BaseVisitor {
+ visitElement(element: Element, bundle: ParsedTranslationBundle): void {
+ if (element.name === 'trans-unit') {
+ this.visitTransUnitElement(element, bundle);
} else {
- return visitAll(this, element.children);
+ visitAll(this, element.children, bundle);
}
}
-}
-class XliffTranslationVisitor extends BaseVisitor {
- private translations: Record<ɵMessageId, ɵParsedTranslation> = {};
+ private visitTransUnitElement(element: Element, bundle: ParsedTranslationBundle): void {
+ // Error if no `id` attribute
+ const id = getAttribute(element, 'id');
+ if (id === undefined) {
+ addParseDiagnostic(
+ bundle.diagnostics, element.sourceSpan,
+ `Missing required "id" attribute on element.`, ParseErrorLevel.ERROR);
+ return;
+ }
- static extractTranslations(file: Element): Record {
- const visitor = new this();
- visitAll(visitor, file.children);
- return visitor.translations;
- }
+ // Error if there is already a translation with the same id
+ if (bundle.translations[id] !== undefined) {
+ addParseDiagnostic(
+ bundle.diagnostics, element.sourceSpan, `Duplicated translations for message "${id}"`,
+ ParseErrorLevel.ERROR);
+ return;
+ }
- visitElement(element: Element): any {
- if (element.name === 'trans-unit') {
- const id = getAttrOrThrow(element, 'id');
- if (this.translations[id] !== undefined) {
- throw new TranslationParseError(
- element.sourceSpan, `Duplicated translations for message "${id}"`);
- }
+ // Error if there is no `` child element
+ const targetMessage = element.children.find(isNamedElement('target'));
+ if (targetMessage === undefined) {
+ addParseDiagnostic(
+ bundle.diagnostics, element.sourceSpan, 'Missing required element',
+ ParseErrorLevel.ERROR);
+ return;
+ }
- const targetMessage = element.children.find(isTargetElement);
- if (targetMessage === undefined) {
- throw new TranslationParseError(element.sourceSpan, 'Missing required element');
+ try {
+ bundle.translations[id] = serializeTargetMessage(targetMessage);
+ } catch (e) {
+ // Capture any errors from serialize the target message
+ if (e.span && e.msg && e.level) {
+ addParseDiagnostic(bundle.diagnostics, e.span, e.msg, e.level);
+ } else {
+ throw e;
}
- this.translations[id] = serializeTargetMessage(targetMessage);
- } else {
- return visitAll(this, element.children);
}
}
}
@@ -98,7 +160,3 @@ function serializeTargetMessage(source: Element): ɵParsedTranslation {
});
return serializer.serialize(parseInnerRange(source));
}
-
-function isTargetElement(node: Node): node is Element {
- return node instanceof Element && node.name === 'target';
-}
diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser.ts
index 3530150ea893..debeafcd1011 100644
--- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser.ts
+++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xliff2_translation_parser.ts
@@ -5,19 +5,16 @@
* 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 {Element, Node, XmlParser, visitAll} from '@angular/compiler';
-import {ɵMessageId, ɵParsedTranslation} from '@angular/localize';
-import {extname} from 'path';
+import {Element, Node, ParseErrorLevel, visitAll} from '@angular/compiler';
+import {ɵParsedTranslation} from '@angular/localize';
+import {Diagnostics} from '../../../diagnostics';
import {BaseVisitor} from '../base_visitor';
import {MessageSerializer} from '../message_serialization/message_serializer';
import {TargetMessageRenderer} from '../message_serialization/target_message_renderer';
-import {TranslationParseError} from './translation_parse_error';
import {ParsedTranslationBundle, TranslationParser} from './translation_parser';
-import {getAttrOrThrow, getAttribute, parseInnerRange} from './translation_utils';
-
-const XLIFF_2_0_NS_REGEX = /xmlns="urn:oasis:names:tc:xliff:document:2.0"/;
+import {XmlTranslationParserHint, addParseDiagnostic, addParseError, canParseXml, getAttribute, isNamedElement, parseInnerRange} from './translation_utils';
/**
* A translation parser that can load translations from XLIFF 2 files.
@@ -25,84 +22,126 @@ const XLIFF_2_0_NS_REGEX = /xmlns="urn:oasis:names:tc:xliff:document:2.0"/;
* http://docs.oasis-open.org/xliff/xliff-core/v2.0/os/xliff-core-v2.0-os.html
*
*/
-export class Xliff2TranslationParser implements TranslationParser {
- canParse(filePath: string, contents: string): boolean {
- return (extname(filePath) === '.xlf') && XLIFF_2_0_NS_REGEX.test(contents);
+export class Xliff2TranslationParser implements TranslationParser {
+ canParse(filePath: string, contents: string): XmlTranslationParserHint|false {
+ return canParseXml(filePath, contents, 'xliff', {version: '2.0'});
}
- parse(filePath: string, contents: string): ParsedTranslationBundle {
- const xmlParser = new XmlParser();
- const xml = xmlParser.parse(contents, filePath);
- const bundle = Xliff2TranslationBundleVisitor.extractBundle(xml.rootNodes);
- if (bundle === undefined) {
- throw new Error(`Unable to parse "${filePath}" as XLIFF 2.0 format.`);
+ parse(filePath: string, contents: string, hint?: XmlTranslationParserHint):
+ ParsedTranslationBundle {
+ if (hint) {
+ return this.extractBundle(hint);
+ } else {
+ return this.extractBundleDeprecated(filePath, contents);
}
- return bundle;
}
-}
-interface BundleVisitorContext {
- parsedLocale?: string;
-}
+ private extractBundle({element, errors}: XmlTranslationParserHint): ParsedTranslationBundle {
+ const diagnostics = new Diagnostics();
+ errors.forEach(e => addParseError(diagnostics, e));
-class Xliff2TranslationBundleVisitor extends BaseVisitor {
- private bundle: ParsedTranslationBundle|undefined;
+ const locale = getAttribute(element, 'trgLang');
+ const files = element.children.filter(isFileElement);
+ if (files.length === 0) {
+ addParseDiagnostic(
+ diagnostics, element.sourceSpan, 'No elements found in ',
+ ParseErrorLevel.WARNING);
+ } else if (files.length > 1) {
+ addParseDiagnostic(
+ diagnostics, files[1].sourceSpan, 'More than one element found in ',
+ ParseErrorLevel.WARNING);
+ }
- static extractBundle(xliff: Node[]): ParsedTranslationBundle|undefined {
- const visitor = new this();
- visitAll(visitor, xliff, {});
- return visitor.bundle;
+ const bundle = {locale, translations: {}, diagnostics};
+ const translationVisitor = new Xliff2TranslationVisitor();
+ for (const file of files) {
+ visitAll(translationVisitor, file.children, {bundle});
+ }
+ return bundle;
}
- visitElement(element: Element, {parsedLocale}: BundleVisitorContext): any {
- if (element.name === 'xliff') {
- parsedLocale = getAttribute(element, 'trgLang');
- return visitAll(this, element.children, {parsedLocale});
- } else if (element.name === 'file') {
- this.bundle = {
- locale: parsedLocale,
- translations: Xliff2TranslationVisitor.extractTranslations(element)
- };
- } else {
- return visitAll(this, element.children, {parsedLocale});
+ private extractBundleDeprecated(filePath: string, contents: string) {
+ const hint = this.canParse(filePath, contents);
+ if (!hint) {
+ throw new Error(`Unable to parse "${filePath}" as XLIFF 2.0 format.`);
+ }
+ const bundle = this.extractBundle(hint);
+ if (bundle.diagnostics.hasErrors) {
+ const message =
+ bundle.diagnostics.formatDiagnostics(`Failed to parse "${filePath}" as XLIFF 2.0 format`);
+ throw new Error(message);
}
+ return bundle;
}
}
-class Xliff2TranslationVisitor extends BaseVisitor {
- private translations: Record<ɵMessageId, ɵParsedTranslation> = {};
- static extractTranslations(file: Element): Record {
- const visitor = new this();
- visitAll(visitor, file.children);
- return visitor.translations;
- }
+interface TranslationVisitorContext {
+ unit?: string;
+ bundle: ParsedTranslationBundle;
+}
- visitElement(element: Element, context: any): any {
+class Xliff2TranslationVisitor extends BaseVisitor {
+ visitElement(element: Element, {bundle, unit}: TranslationVisitorContext): any {
if (element.name === 'unit') {
- const externalId = getAttrOrThrow(element, 'id');
- if (this.translations[externalId] !== undefined) {
- throw new TranslationParseError(
- element.sourceSpan, `Duplicated translations for message "${externalId}"`);
- }
- visitAll(this, element.children, {unit: externalId});
+ this.visitUnitElement(element, bundle);
} else if (element.name === 'segment') {
- assertTranslationUnit(element, context);
- const targetMessage = element.children.find(isTargetElement);
- if (targetMessage === undefined) {
- throw new TranslationParseError(element.sourceSpan, 'Missing required element');
- }
- this.translations[context.unit] = serializeTargetMessage(targetMessage);
+ this.visitSegmentElement(element, bundle, unit);
} else {
- return visitAll(this, element.children);
+ visitAll(this, element.children, {bundle, unit});
}
}
-}
-function assertTranslationUnit(segment: Element, context: any) {
- if (context === undefined || context.unit === undefined) {
- throw new TranslationParseError(
- segment.sourceSpan, 'Invalid element: should be a child of a element.');
+ private visitUnitElement(element: Element, bundle: ParsedTranslationBundle): void {
+ // Error if no `id` attribute
+ const externalId = getAttribute(element, 'id');
+ if (externalId === undefined) {
+ addParseDiagnostic(
+ bundle.diagnostics, element.sourceSpan,
+ `Missing required "id" attribute on element.`, ParseErrorLevel.ERROR);
+ return;
+ }
+
+ // Error if there is already a translation with the same id
+ if (bundle.translations[externalId] !== undefined) {
+ addParseDiagnostic(
+ bundle.diagnostics, element.sourceSpan,
+ `Duplicated translations for message "${externalId}"`, ParseErrorLevel.ERROR);
+ return;
+ }
+
+ visitAll(this, element.children, {bundle, unit: externalId});
+ }
+
+ private visitSegmentElement(
+ element: Element, bundle: ParsedTranslationBundle, unit: string|undefined): void {
+ // A `` element must be below a `` element
+ if (unit === undefined) {
+ addParseDiagnostic(
+ bundle.diagnostics, element.sourceSpan,
+ 'Invalid element: should be a child of a element.',
+ ParseErrorLevel.ERROR);
+ return;
+ }
+
+ const targetMessage = element.children.find(isNamedElement('target'));
+ if (targetMessage === undefined) {
+ addParseDiagnostic(
+ bundle.diagnostics, element.sourceSpan, 'Missing required element',
+ ParseErrorLevel.ERROR);
+ return;
+ }
+
+ try {
+ bundle.translations[unit] = serializeTargetMessage(targetMessage);
+ } catch (e) {
+ // Capture any errors from serialize the target message
+ if (e.span && e.msg && e.level) {
+ addParseDiagnostic(bundle.diagnostics, e.span, e.msg, e.level);
+ } else {
+ throw e;
+ }
+ }
}
}
@@ -116,6 +155,6 @@ function serializeTargetMessage(source: Element): ɵParsedTranslation {
return serializer.serialize(parseInnerRange(source));
}
-function isTargetElement(node: Node): node is Element {
- return node instanceof Element && node.name === 'target';
+function isFileElement(node: Node): node is Element {
+ return node instanceof Element && node.name === 'file';
}
diff --git a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xtb_translation_parser.ts b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xtb_translation_parser.ts
index a0358f349118..c3a6388d185b 100644
--- a/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xtb_translation_parser.ts
+++ b/packages/localize/src/tools/src/translate/translation_files/translation_parsers/xtb_translation_parser.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 {Element, Node, XmlParser, visitAll} from '@angular/compiler';
+import {Element, ParseErrorLevel, visitAll} from '@angular/compiler';
import {ɵParsedTranslation} from '@angular/localize';
import {extname} from 'path';
@@ -14,83 +14,100 @@ import {BaseVisitor} from '../base_visitor';
import {MessageSerializer} from '../message_serialization/message_serializer';
import {TargetMessageRenderer} from '../message_serialization/target_message_renderer';
-import {TranslationParseError} from './translation_parse_error';
import {ParsedTranslationBundle, TranslationParser} from './translation_parser';
-import {getAttrOrThrow, parseInnerRange} from './translation_utils';
-
+import {XmlTranslationParserHint, addParseDiagnostic, addParseError, canParseXml, getAttribute, parseInnerRange} from './translation_utils';
/**
* A translation parser that can load XB files.
*/
-export class XtbTranslationParser implements TranslationParser {
- constructor(private diagnostics: Diagnostics) {}
-
- canParse(filePath: string, contents: string): boolean {
+export class XtbTranslationParser implements TranslationParser {
+ canParse(filePath: string, contents: string): XmlTranslationParserHint|false {
const extension = extname(filePath);
- return (extension === '.xtb' || extension === '.xmb') &&
- contents.includes(' attr.name === 'lang');
+ const bundle: ParsedTranslationBundle = {
+ locale: langAttr && langAttr.value,
+ translations: {},
+ diagnostics: new Diagnostics()
+ };
+ errors.forEach(e => addParseError(bundle.diagnostics, e));
+
+ const bundleVisitor = new XtbVisitor();
+ visitAll(bundleVisitor, element.children, bundle);
+ return bundle;
}
- constructor(private diagnostics: Diagnostics) { super(); }
+ private extractBundleDeprecated(filePath: string, contents: string) {
+ const hint = this.canParse(filePath, contents);
+ if (!hint) {
+ throw new Error(`Unable to parse "${filePath}" as XMB/XTB format.`);
+ }
+ const bundle = this.extractBundle(hint);
+ if (bundle.diagnostics.hasErrors) {
+ const message =
+ bundle.diagnostics.formatDiagnostics(`Failed to parse "${filePath}" as XMB/XTB format`);
+ throw new Error(message);
+ }
+ return bundle;
+ }
+}
- visitElement(element: Element, bundle: ParsedTranslationBundle|undefined): any {
+class XtbVisitor extends BaseVisitor {
+ visitElement(element: Element, bundle: ParsedTranslationBundle): any {
switch (element.name) {
- case 'translationbundle':
- if (bundle) {
- throw new TranslationParseError(
- element.sourceSpan, ' elements can not be nested');
+ case 'translation':
+ // Error if no `id` attribute
+ const id = getAttribute(element, 'id');
+ if (id === undefined) {
+ addParseDiagnostic(
+ bundle.diagnostics, element.sourceSpan,
+ `Missing required "id" attribute on element.`, ParseErrorLevel.ERROR);
+ return;
}
- const langAttr = element.attrs.find((attr) => attr.name === 'lang');
- bundle = {locale: langAttr && langAttr.value, translations: {}};
- visitAll(this, element.children, bundle);
- return bundle;
- case 'translation':
- if (!bundle) {
- throw new TranslationParseError(
- element.sourceSpan, ' must be inside a ');
+ // Error if there is already a translation with the same id
+ if (bundle.translations[id] !== undefined) {
+ addParseDiagnostic(
+ bundle.diagnostics, element.sourceSpan, `Duplicated translations for message "${id}"`,
+ ParseErrorLevel.ERROR);
+ return;
}
- const id = getAttrOrThrow(element, 'id');
- if (bundle.translations.hasOwnProperty(id)) {
- throw new TranslationParseError(
- element.sourceSpan, `Duplicated translations for message "${id}"`);
- } else {
- try {
- bundle.translations[id] = serializeTargetMessage(element);
- } catch (error) {
- if (typeof error === 'string') {
- this.diagnostics.warn(
- `Could not parse message with id "${id}" - perhaps it has an unrecognised ICU format?\n` +
- error);
- } else {
- throw error;
- }
+
+ try {
+ bundle.translations[id] = serializeTargetMessage(element);
+ } catch (error) {
+ if (typeof error === 'string') {
+ bundle.diagnostics.warn(
+ `Could not parse message with id "${id}" - perhaps it has an unrecognised ICU format?\n` +
+ error);
+ } else if (error.span && error.msg && error.level) {
+ addParseDiagnostic(bundle.diagnostics, error.span, error.msg, error.level);
+ } else {
+ throw error;
}
}
break;
default:
- throw new TranslationParseError(element.sourceSpan, 'Unexpected tag');
+ addParseDiagnostic(
+ bundle.diagnostics, element.sourceSpan, `Unexpected <${element.name}> tag.`,
+ ParseErrorLevel.ERROR);
}
}
}
diff --git a/packages/localize/src/tools/src/translate/translator.ts b/packages/localize/src/tools/src/translate/translator.ts
index 1cda8b75d01f..74ccebada59b 100644
--- a/packages/localize/src/tools/src/translate/translator.ts
+++ b/packages/localize/src/tools/src/translate/translator.ts
@@ -19,6 +19,7 @@ import {OutputPathFn} from './output_path';
export interface TranslationBundle {
locale: string;
translations: Record<ɵMessageId, ɵParsedTranslation>;
+ diagnostics?: Diagnostics;
}
/**
diff --git a/packages/localize/src/tools/test/diagnostics_spec.ts b/packages/localize/src/tools/test/diagnostics_spec.ts
new file mode 100644
index 000000000000..c2c32a8644b7
--- /dev/null
+++ b/packages/localize/src/tools/test/diagnostics_spec.ts
@@ -0,0 +1,48 @@
+/**
+ * @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 {Diagnostics} from '../src/diagnostics';
+
+describe('Diagnostics', () => {
+ describe('formatDiagnostics', () => {
+ it('should just return the message passed in if there are no errors nor warnings', () => {
+ const diagnostics = new Diagnostics();
+ expect(diagnostics.formatDiagnostics('This is a message')).toEqual('This is a message');
+ });
+
+ it('should return a string with all the errors listed', () => {
+ const diagnostics = new Diagnostics();
+ diagnostics.error('Error 1');
+ diagnostics.error('Error 2');
+ diagnostics.error('Error 3');
+ expect(diagnostics.formatDiagnostics('This is a message'))
+ .toEqual('This is a message\nERRORS:\n - Error 1\n - Error 2\n - Error 3');
+ });
+
+ it('should return a string with all the warnings listed', () => {
+ const diagnostics = new Diagnostics();
+ diagnostics.warn('Warning 1');
+ diagnostics.warn('Warning 2');
+ diagnostics.warn('Warning 3');
+ expect(diagnostics.formatDiagnostics('This is a message'))
+ .toEqual('This is a message\nWARNINGS:\n - Warning 1\n - Warning 2\n - Warning 3');
+ });
+
+ it('should return a string with all the errors and warnings listed', () => {
+ const diagnostics = new Diagnostics();
+ diagnostics.warn('Warning 1');
+ diagnostics.warn('Warning 2');
+ diagnostics.error('Error 1');
+ diagnostics.error('Error 2');
+ diagnostics.warn('Warning 3');
+ diagnostics.error('Error 3');
+ expect(diagnostics.formatDiagnostics('This is a message'))
+ .toEqual(
+ 'This is a message\nERRORS:\n - Error 1\n - Error 2\n - Error 3\nWARNINGS:\n - Warning 1\n - Warning 2\n - Warning 3');
+ });
+ });
+});
diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts
index 0fd0a5c29983..0d9995d55747 100644
--- a/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts
+++ b/packages/localize/src/tools/test/translate/translation_files/translation_loader_spec.ts
@@ -58,8 +58,8 @@ describe('TranslationLoader', () => {
const result =
loader.loadBundles(['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], []);
expect(result).toEqual([
- {locale: 'pl', translations},
- {locale: 'pl', translations},
+ {locale: 'pl', translations, diagnostics: new Diagnostics()},
+ {locale: 'pl', translations, diagnostics: new Diagnostics()},
]);
});
@@ -71,8 +71,8 @@ describe('TranslationLoader', () => {
const result = loader.loadBundles(
['/src/locale/messages.en.xlf', '/src/locale/messages.fr.xlf'], ['en', 'fr']);
expect(result).toEqual([
- {locale: 'en', translations},
- {locale: 'fr', translations},
+ {locale: 'en', translations, diagnostics: new Diagnostics()},
+ {locale: 'fr', translations, diagnostics: new Diagnostics()},
]);
});
@@ -127,6 +127,6 @@ class MockTranslationParser implements TranslationParser {
parse(filePath: string, fileContents: string) {
this.log.push(`parse(${filePath}, ${fileContents})`);
- return {locale: this._locale, translations: this._translations};
+ return {locale: this._locale, translations: this._translations, diagnostics: new Diagnostics()};
}
}
\ No newline at end of file
diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts
index 594d2fabc71f..6edfa714b9df 100644
--- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts
+++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff1_translation_parser_spec.ts
@@ -10,35 +10,55 @@ import {Xliff1TranslationParser} from '../../../../src/translate/translation_fil
describe('Xliff1TranslationParser', () => {
describe('canParse()', () => {
- it('should return true if the file extension is `.xlf` and it contains the XLIFF namespace',
+ it('should return true only if the file contains an element with version="1.2" attribute',
() => {
const parser = new Xliff1TranslationParser();
expect(parser.canParse(
'/some/file.xlf',
''))
- .toBe(true);
+ .toBeTruthy();
expect(parser.canParse(
'/some/file.json',
''))
- .toBe(false);
+ .toBeTruthy();
+ expect(parser.canParse('/some/file.xliff', '')).toBeTruthy();
+ expect(parser.canParse('/some/file.json', '')).toBeTruthy();
+ expect(parser.canParse('/some/file.xlf', '')).toBe(false);
+ expect(parser.canParse('/some/file.xlf', '')).toBe(false);
expect(parser.canParse('/some/file.xlf', '')).toBe(false);
expect(parser.canParse('/some/file.json', '')).toBe(false);
});
});
- describe('parse()', () => {
- it('should extract the locale from the file contents', () => {
- const XLIFF = `
+ describe('parse() [without hint]', () => {
+ it('should extract the locale from the last `` element to contain a `target-language` attribute',
+ () => {
+ const XLIFF = `
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
`;
- const parser = new Xliff1TranslationParser();
- const result = parser.parse('/some/file.xlf', XLIFF);
- expect(result.locale).toEqual('fr');
- });
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.locale).toEqual('de');
+ });
it('should return an undefined locale if there is no locale in the file', () => {
const XLIFF = `
@@ -433,18 +453,71 @@ describe('Xliff1TranslationParser', () => {
.toEqual(ɵmakeParsedTranslation(['Weiter']));
});
+ it('should merge messages from each `` element', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * translatable attribute
+ * ```
+
+ * ```
+ * translatable element with placeholders {{ interpolation}}
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ translatable attribute
+ etubirtta elbatalsnart
+
+ file.ts
+ 1
+
+
+
+
+
+
+
+ translatable element with placeholders
+ tnemele elbatalsnart sredlohecalp htiw
+
+ file.ts
+ 2
+
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const result = parser.parse('/some/file.xlf', XLIFF);
+
+ expect(result.translations[ɵcomputeMsgId('translatable attribute')])
+ .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart']));
+ expect(
+ result.translations[ɵcomputeMsgId(
+ 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')])
+ .toEqual(ɵmakeParsedTranslation(
+ ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''],
+ ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
+ });
+
describe('[structure errors]', () => {
it('should throw when a trans-unit has no translation', () => {
- const XLIFF = `
-
-
-
-
-
-
-
-
- `;
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+ `;
expect(() => {
const parser = new Xliff1TranslationParser();
@@ -454,17 +527,18 @@ describe('Xliff1TranslationParser', () => {
it('should throw when a trans-unit has no id attribute', () => {
- const XLIFF = `
-
-
-
-
-
-
-
-
-
- `;
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+
+ `;
expect(() => {
const parser = new Xliff1TranslationParser();
@@ -473,21 +547,22 @@ describe('Xliff1TranslationParser', () => {
});
it('should throw on duplicate trans-unit id', () => {
- const XLIFF = `
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
expect(() => {
const parser = new Xliff1TranslationParser();
@@ -498,17 +573,18 @@ describe('Xliff1TranslationParser', () => {
describe('[message errors]', () => {
it('should throw on unknown message tags', () => {
- const XLIFF = `
-
-
-
-
-
- msg should contain only ph tags
-
-
-
- `;
+ const XLIFF = `
+
+
+
+
+
+
+ msg should contain only ph tags
+
+
+
+ `;
expect(() => {
const parser = new Xliff1TranslationParser();
@@ -517,17 +593,18 @@ describe('Xliff1TranslationParser', () => {
});
it('should throw when a placeholder misses an id attribute', () => {
- const XLIFF = `
-
-
-
-
-
-
-
-
-
- `;
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+
+ `;
expect(() => {
const parser = new Xliff1TranslationParser();
@@ -536,4 +613,698 @@ describe('Xliff1TranslationParser', () => {
});
});
});
+
+ describe('parse() [with hint]', () => {
+ it('should extract the locale from the last `` element to contain a `target-language` attribute',
+ () => {
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.locale).toEqual('de');
+ });
+
+ it('should return an undefined locale if there is no locale in the file', () => {
+ const XLIFF = `
+
+
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.locale).toBeUndefined();
+ });
+
+ it('should extract basic messages', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * translatable attribute
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ translatable attribute
+ etubirtta elbatalsnart
+
+ file.ts
+ 1
+
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('translatable attribute')])
+ .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart']));
+ });
+
+ it('should extract translations with simple placeholders', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * translatable element with placeholders {{ interpolation}}
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ translatable element with placeholders
+ tnemele elbatalsnart sredlohecalp htiw
+
+ file.ts
+ 2
+
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(
+ result.translations[ɵcomputeMsgId(
+ 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')])
+ .toEqual(ɵmakeParsedTranslation(
+ ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''],
+ ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
+ });
+
+ it('should extract translations with placeholders containing hyphens', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ *
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ Welcome
+
+ src/app/app.component.html
+ 1
+
+ Translate
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ const id =
+ ɵcomputeMsgId('{$START_TAG_APP_MY_COMPONENT}{$CLOSE_TAG_APP_MY_COMPONENT} Welcome');
+ expect(result.translations[id]).toEqual(ɵmakeParsedTranslation(['', '', ' Translate'], [
+ 'START_TAG_APP_MY_COMPONENT', 'CLOSE_TAG_APP_MY_COMPONENT'
+ ]));
+ });
+
+ it('should extract translations with simple ICU expressions', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * {VAR_PLURAL, plural, =0 {
test
} }
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ {VAR_PLURAL, plural, =0 { test } }
+ {VAR_PLURAL, plural, =0 { TEST } }
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId(
+ '{VAR_PLURAL, plural, =0 {{START_PARAGRAPH}test{CLOSE_PARAGRAPH}}}')])
+ .toEqual(ɵmakeParsedTranslation(
+ ['{VAR_PLURAL, plural, =0 {{START_PARAGRAPH}TEST{CLOSE_PARAGRAPH}}}'], []));
+ });
+
+ it('should extract translations with duplicate source messages', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * foo
+ * foo
+ * foo
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ foo
+ oof
+
+ file.ts
+ 3
+
+ d
+ m
+
+
+ foo
+ toto
+
+ file.ts
+ 4
+
+ d
+ m
+
+
+ foo
+ tata
+
+ file.ts
+ 5
+
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('foo')]).toEqual(ɵmakeParsedTranslation(['oof']));
+ expect(result.translations['i']).toEqual(ɵmakeParsedTranslation(['toto']));
+ expect(result.translations['bar']).toEqual(ɵmakeParsedTranslation(['tata']));
+ });
+
+ it('should extract translations with only placeholders, which are re-ordered', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ *
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+
+
+
+
+ file.ts
+ 6
+
+ ph names
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('{$LINE_BREAK}{$TAG_IMG}{$TAG_IMG_1}')])
+ .toEqual(
+ ɵmakeParsedTranslation(['', '', '', ''], ['TAG_IMG_1', 'TAG_IMG', 'LINE_BREAK']));
+ });
+
+ it('should extract translations with empty target', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * hello
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ hello
+
+
+ file.ts
+ 6
+
+ ph names
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('hello {$START_TAG_SPAN}{$CLOSE_TAG_SPAN}')])
+ .toEqual(ɵmakeParsedTranslation(['']));
+ });
+
+ it('should extract translations with deeply nested ICUs', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * Test: { count, plural, =0 { { sex, select, other {deeply nested
}} } =other {a lot}}
+ * ```
+ *
+ * Note that the message gets split into two translation units:
+ * * The first one contains the outer message with an `ICU` placeholder
+ * * The second one is the ICU expansion itself
+ *
+ * Note that special markers `VAR_PLURAL` and `VAR_SELECT` are added, which are then replaced
+ * by IVY at runtime with the actual values being rendered by the ICU expansion.
+ */
+ const XLIFF = `
+
+
+
+
+ Test:
+ Le test:
+
+ file.ts
+ 11
+
+
+
+ {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other { deeply nested }}} =other {a lot}}
+ {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other { profondément imbriqué }}} =other {beaucoup}}
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('Test: {$ICU}')])
+ .toEqual(ɵmakeParsedTranslation(['Le test: ', ''], ['ICU']));
+
+ expect(
+ result.translations[ɵcomputeMsgId(
+ '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}deeply nested{CLOSE_PARAGRAPH}}}} =other {beaucoup}}')])
+ .toEqual(ɵmakeParsedTranslation([
+ '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}profondément imbriqué{CLOSE_PARAGRAPH}}}} =other {beaucoup}}'
+ ]));
+ });
+
+ it('should extract translations containing multiple lines', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * multi
+ * lines
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ multi\nlines
+ multi\nlignes
+
+ file.ts
+ 12
+
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('multi\nlines')])
+ .toEqual(ɵmakeParsedTranslation(['multi\nlignes']));
+ });
+
+ it('should extract translations with elements', () => {
+ const XLIFF = `
+
+
+
+
+ First sentence.
+
+ Should not be parsed
+
+ Translated first sentence .
+
+
+ First sentence. Second sentence.
+
+ Should not be parsed
+
+ Translated first sentence .
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations['mrk-test'])
+ .toEqual(ɵmakeParsedTranslation(['Translated first sentence.']));
+
+ expect(result.translations['mrk-test2'])
+ .toEqual(ɵmakeParsedTranslation(['Translated first sentence.']));
+ });
+
+ it('should ignore alt-trans targets', () => {
+ const XLIFF = `
+
+
+
+
+ Continue
+ Weiter
+
+ src/app/auth/registration-form/registration-form.component.html
+ 69
+
+
+
+ Content
+ Content
+
+
+
+
+ `;
+
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.translations['registration.submit'])
+ .toEqual(ɵmakeParsedTranslation(['Weiter']));
+ });
+
+ it('should merge messages from each `` element', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * translatable attribute
+ * ```
+ *
+ * ```
+ * translatable element with placeholders {{ interpolation}}
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ translatable attribute
+ etubirtta elbatalsnart
+
+ file.ts
+ 1
+
+
+
+
+
+
+
+ translatable element with placeholders
+ tnemele elbatalsnart sredlohecalp htiw
+
+ file.ts
+ 2
+
+
+
+
+ `;
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('translatable attribute')])
+ .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart']));
+ expect(
+ result.translations[ɵcomputeMsgId(
+ 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')])
+ .toEqual(ɵmakeParsedTranslation(
+ ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''],
+ ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
+ });
+
+ describe('[structure errors]', () => {
+ it('should provide a diagnostic error when a trans-unit has no translation', () => {
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+ `;
+
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message)
+ .toEqual(
+ `Missing required element ("ge="en" target-language="fr" datatype="plaintext" original="ng2.template">\n` +
+ ` \n` +
+ ` [ERROR ->]\n` +
+ ` \n` +
+ ` \n` +
+ `"): /some/file.xlf@5:10`);
+ });
+
+
+ it('should provide a diagnostic error when a trans-unit has no id attribute', () => {
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message)
+ .toEqual(
+ `Missing required "id" attribute on element. ("ge="en" target-language="fr" datatype="plaintext" original="ng2.template">\n` +
+ ` \n` +
+ ` [ERROR ->]\n` +
+ ` \n` +
+ ` \n` +
+ `"): /some/file.xlf@5:10`);
+ });
+
+ it('should provide a diagnostic error on duplicate trans-unit id', () => {
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message)
+ .toEqual(
+ `Duplicated translations for message "deadbeef" ("\n` +
+ ` \n` +
+ ` \n` +
+ ` [ERROR ->]\n` +
+ ` \n` +
+ ` \n` +
+ `"): /some/file.xlf@9:10`);
+ });
+ });
+
+ describe('[message errors]', () => {
+ it('should provide a diagnostic error on unknown message tags', () => {
+ const XLIFF = `
+
+
+
+
+
+
+ msg should contain only ph tags
+
+
+
+ `;
+
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message)
+ .toEqual(
+ `Invalid element found in message. ("\n` +
+ ` \n` +
+ ` \n` +
+ ` [ERROR ->]msg should contain only ph tags \n` +
+ ` \n` +
+ ` \n` +
+ `"): /some/file.xlf@7:20`);
+ });
+
+ it('should provide a diagnostic error when a placeholder misses an id attribute', () => {
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ const parser = new Xliff1TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message)
+ .toEqual(
+ `Missing required "id" attribute: ("\n` +
+ ` \n` +
+ ` \n` +
+ ` [ERROR ->] \n` +
+ ` \n` +
+ ` \n` +
+ `"): /some/file.xlf@7:20`);
+ });
+ });
+ });
});
diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2_translation_parser_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2_translation_parser_spec.ts
index 10381a3b2bcd..869149b1c901 100644
--- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2_translation_parser_spec.ts
+++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xliff2_translation_parser_spec.ts
@@ -10,24 +10,27 @@ import {Xliff2TranslationParser} from '../../../../src/translate/translation_fil
describe('Xliff2TranslationParser', () => {
describe('canParse()', () => {
- it('should return true if the file extension is `.xlf` and it contains the XLIFF namespace', () => {
- const parser = new Xliff2TranslationParser();
- expect(
- parser.canParse(
- '/some/file.xlf',
- ''))
- .toBe(true);
- expect(
- parser.canParse(
- '/some/file.json',
- ''))
- .toBe(false);
- expect(parser.canParse('/some/file.xlf', '')).toBe(false);
- expect(parser.canParse('/some/file.json', '')).toBe(false);
- });
+ it('should return true if the file contains an element with version="2.0" attribute',
+ () => {
+ const parser = new Xliff2TranslationParser();
+ expect(parser.canParse(
+ '/some/file.xlf',
+ ''))
+ .toBeTruthy();
+ expect(parser.canParse(
+ '/some/file.json',
+ ''))
+ .toBeTruthy();
+ expect(parser.canParse('/some/file.xliff', '')).toBeTruthy();
+ expect(parser.canParse('/some/file.json', '')).toBeTruthy();
+ expect(parser.canParse('/some/file.xlf', '')).toBe(false);
+ expect(parser.canParse('/some/file.xlf', '')).toBe(false);
+ expect(parser.canParse('/some/file.xlf', '')).toBe(false);
+ expect(parser.canParse('/some/file.json', '')).toBe(false);
+ });
});
- describe('parse()', () => {
+ describe('parse() [without hint]', () => {
it('should extract the locale from the file contents', () => {
const XLIFF = `
@@ -84,7 +87,7 @@ describe('Xliff2TranslationParser', () => {
* Source HTML:
*
* ```
- * translatable element >with placeholders {{ interpolation}}
+ * translatable element with placeholders {{ interpolation}}
* ```
*/
const XLIFF = `
@@ -370,6 +373,57 @@ describe('Xliff2TranslationParser', () => {
.toEqual(ɵmakeParsedTranslation(['Translated first sentence.']));
});
+ it('should merge messages from each `` element', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * translatable attribute
+ * ```
+ *
+ * ```
+ * translatable element with placeholders {{ interpolation}}
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ file.ts:2
+
+
+ translatable attribute
+ etubirtta elbatalsnart
+
+
+
+
+
+
+ file.ts:3
+
+
+ translatable element with placeholders
+ tnemele elbatalsnart sredlohecalp htiw
+
+
+
+ `;
+ const parser = new Xliff2TranslationParser();
+ const result = parser.parse('/some/file.xlf', XLIFF);
+
+ expect(result.translations[ɵcomputeMsgId('translatable attribute', '')])
+ .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart']));
+ expect(
+ result.translations[ɵcomputeMsgId(
+ 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')])
+ .toEqual(ɵmakeParsedTranslation(
+ ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''],
+ ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
+
+ });
+
describe('[structure errors]', () => {
it('should throw when a trans-unit has no translation', () => {
const XLIFF = `
@@ -475,4 +529,605 @@ describe('Xliff2TranslationParser', () => {
});
});
});
+
+ describe('parse() [with hint]', () => {
+ it('should extract the locale from the file contents', () => {
+ const XLIFF = `
+
+
+
+ `;
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.locale).toEqual('fr');
+ });
+
+ it('should return undefined locale if there is no locale in the file', () => {
+ const XLIFF = `
+
+
+
+ `;
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.locale).toBeUndefined();
+ });
+
+ it('should extract basic messages', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * translatable attribute
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ file.ts:2
+
+
+ translatable attribute
+ etubirtta elbatalsnart
+
+
+
+ `;
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('translatable attribute', '')])
+ .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart']));
+ });
+
+ it('should extract translations with simple placeholders', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * translatable element >with placeholders {{ interpolation}}
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ file.ts:3
+
+
+ translatable element with placeholders
+ tnemele elbatalsnart sredlohecalp htiw
+
+
+
+ `;
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(
+ result.translations[ɵcomputeMsgId(
+ 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')])
+ .toEqual(ɵmakeParsedTranslation(
+ ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''],
+ ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
+ });
+
+ it('should extract translations with simple ICU expressions', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * {VAR_PLURAL, plural, =0 {
test
} }
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ file.ts:4
+
+
+ {VAR_PLURAL, plural, =0 {test } }
+ {VAR_PLURAL, plural, =0 {TEST } }
+
+
+
+ `;
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId(
+ '{VAR_PLURAL, plural, =0 {{START_PARAGRAPH}test{CLOSE_PARAGRAPH}}}')])
+ .toEqual(ɵmakeParsedTranslation(
+ ['{VAR_PLURAL, plural, =0 {{START_PARAGRAPH}TEST{CLOSE_PARAGRAPH}}}'], []));
+ });
+
+ it('should extract translations with duplicate source messages', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * foo
+ * foo
+ * foo
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ d
+ m
+ file.ts:5
+
+
+ foo
+ oof
+
+
+
+
+ d
+ m
+ file.ts:5
+
+
+ foo
+ toto
+
+
+
+
+ d
+ m
+ file.ts:5
+
+
+ foo
+ tata
+
+
+
+ `;
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('foo')]).toEqual(ɵmakeParsedTranslation(['oof']));
+ expect(result.translations['i']).toEqual(ɵmakeParsedTranslation(['toto']));
+ expect(result.translations['bar']).toEqual(ɵmakeParsedTranslation(['tata']));
+ });
+
+ it('should extract translations with only placeholders, which are re-ordered', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ *
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ ph names
+ file.ts:7
+
+
+
+
+
+
+
+ `;
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('{$LINE_BREAK}{$TAG_IMG}{$TAG_IMG_1}')])
+ .toEqual(
+ ɵmakeParsedTranslation(['', '', '', ''], ['TAG_IMG_1', 'TAG_IMG', 'LINE_BREAK']));
+ });
+
+ it('should extract translations with empty target', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * hello
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ empty element
+ file.ts:8
+
+
+ hello
+
+
+
+
+ `;
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('hello {$START_TAG_SPAN}{$CLOSE_TAG_SPAN}')])
+ .toEqual(ɵmakeParsedTranslation(['']));
+ });
+
+ it('should extract translations with deeply nested ICUs', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * Test: { count, plural, =0 { { sex, select, other {deeply nested
}} } =other {a lot}}
+ * ```
+ *
+ * Note that the message gets split into two translation units:
+ * * The first one contains the outer message with an `ICU` placeholder
+ * * The second one is the ICU expansion itself
+ *
+ * Note that special markers `VAR_PLURAL` and `VAR_SELECT` are added, which are then replaced
+ * by IVY at runtime with the actual values being rendered by the ICU expansion.
+ */
+ const XLIFF = `
+
+
+
+
+ file.ts:10
+
+
+ Test:
+ Le test:
+
+
+
+
+ file.ts:10
+
+
+ {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {deeply nested }}} =other {a lot}}
+ {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {profondément imbriqué }}} =other {beaucoup}}
+
+
+
+ `;
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('Test: {$ICU}')])
+ .toEqual(ɵmakeParsedTranslation(['Le test: ', ''], ['ICU']));
+
+ expect(
+ result.translations[ɵcomputeMsgId(
+ '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}deeply nested{CLOSE_PARAGRAPH}}}} =other {beaucoup}}')])
+ .toEqual(ɵmakeParsedTranslation([
+ '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}profondément imbriqué{CLOSE_PARAGRAPH}}}} =other {beaucoup}}'
+ ]));
+ });
+
+ it('should extract translations containing multiple lines', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * multi
+ * lines
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ file.ts:11,12
+
+
+ multi\nlines
+ multi\nlignes
+
+
+
+ `;
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('multi\nlines')])
+ .toEqual(ɵmakeParsedTranslation(['multi\nlignes']));
+ });
+
+ it('should extract translations with elements', () => {
+ const XLIFF = `
+
+
+
+
+ First sentence.
+ Translated first sentence .
+
+
+
+
+ First sentence. Second sentence.
+ Translated first sentence .
+
+
+
+ `;
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations['mrk-test'])
+ .toEqual(ɵmakeParsedTranslation(['Translated first sentence.']));
+
+ expect(result.translations['mrk-test2'])
+ .toEqual(ɵmakeParsedTranslation(['Translated first sentence.']));
+ });
+
+ it('should merge messages from each `` element', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * translatable attribute
+ * ```
+ *
+ * ```
+ * translatable element with placeholders {{ interpolation}}
+ * ```
+ */
+ const XLIFF = `
+
+
+
+
+ file.ts:2
+
+
+ translatable attribute
+ etubirtta elbatalsnart
+
+
+
+
+
+
+ file.ts:3
+
+
+ translatable element with placeholders
+ tnemele elbatalsnart sredlohecalp htiw
+
+
+
+ `;
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+
+ expect(result.translations[ɵcomputeMsgId('translatable attribute', '')])
+ .toEqual(ɵmakeParsedTranslation(['etubirtta elbatalsnart']));
+ expect(
+ result.translations[ɵcomputeMsgId(
+ 'translatable element {$START_BOLD_TEXT}with placeholders{$LOSE_BOLD_TEXT} {$INTERPOLATION}')])
+ .toEqual(ɵmakeParsedTranslation(
+ ['', ' tnemele elbatalsnart ', 'sredlohecalp htiw', ''],
+ ['INTERPOLATION', 'START_BOLD_TEXT', 'CLOSE_BOLD_TEXT']));
+
+ });
+
+ describe('[structure errors]', () => {
+ it('should provide a diagnostic error when a trans-unit has no translation', () => {
+ const XLIFF = `
+
+
+
+
+
+
+
+
+ `;
+
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message).toEqual(`Missing required element ("
+
+
+ [ERROR ->]
+
+
+"): /some/file.xlf@4:12`);
+ });
+
+
+ it('should provide a diagnostic error when a trans-unit has no id attribute', () => {
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+ `;
+
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message)
+ .toEqual(
+ `Missing required "id" attribute on element. ("ocument:2.0" srcLang="en" trgLang="fr">
+
+ [ERROR ->]
+
+
+"): /some/file.xlf@3:10`);
+ });
+
+ it('should provide a diagnostic error on duplicate trans-unit id', () => {
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message)
+ .toEqual(`Duplicated translations for message "deadbeef" ("
+
+
+ [ERROR ->]
+
+
+"): /some/file.xlf@9:10`);
+ });
+ });
+
+ describe('[message errors]', () => {
+ it('should provide a diagnostic error on unknown message tags', () => {
+ const XLIFF = `
+
+
+
+
+
+ msg should contain only ph and pc tags
+
+
+
+ `;
+
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message).toEqual(`Invalid element found in message. ("
+
+
+ [ERROR ->]msg should contain only ph and pc tags
+
+
+"): /some/file.xlf@6:22`);
+ });
+
+ it('should provide a diagnostic error when a placeholder misses an id attribute', () => {
+ const XLIFF = `
+
+
+
+
+
+
+
+
+
+ `;
+
+ const parser = new Xliff2TranslationParser();
+ const hint = parser.canParse('/some/file.xlf', XLIFF);
+ if (!hint) {
+ return fail('expected XLIFF to be valid');
+ }
+ const result = parser.parse('/some/file.xlf', XLIFF, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message)
+ .toEqual(`Missing required "equiv" attribute: ("
+
+
+ [ERROR ->]
+
+
+"): /some/file.xlf@6:22`);
+ });
+ });
+ });
});
diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts
index d07babbb9b1f..dbb44a9a94b3 100644
--- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts
+++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/xtb_translation_parser_spec.ts
@@ -13,24 +13,24 @@ describe('XtbTranslationParser', () => {
describe('canParse()', () => {
it('should return true if the file extension is `.xtb` or `.xmb` and it contains the `` tag',
() => {
- const parser = new XtbTranslationParser(new Diagnostics());
- expect(parser.canParse('/some/file.xtb', '')).toBe(true);
- expect(parser.canParse('/some/file.xmb', '')).toBe(true);
- expect(parser.canParse('/some/file.xtb', '')).toBe(true);
- expect(parser.canParse('/some/file.xmb', '')).toBe(true);
+ const parser = new XtbTranslationParser();
+ expect(parser.canParse('/some/file.xtb', '')).toBeTruthy();
+ expect(parser.canParse('/some/file.xmb', '')).toBeTruthy();
+ expect(parser.canParse('/some/file.xtb', '')).toBeTruthy();
+ expect(parser.canParse('/some/file.xmb', '')).toBeTruthy();
expect(parser.canParse('/some/file.json', '')).toBe(false);
expect(parser.canParse('/some/file.xmb', '')).toBe(false);
expect(parser.canParse('/some/file.xtb', '')).toBe(false);
});
});
- describe('parse()', () => {
+ describe('parse() [without hint]', () => {
it('should extract the locale from the file contents', () => {
const XTB = `
rab
`;
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
const result = parser.parse('/some/file.xtb', XTB);
expect(result.locale).toEqual('fr');
});
@@ -39,17 +39,17 @@ describe('XtbTranslationParser', () => {
const XTB = `
-
+
-
+
]>
rab
`;
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
const result = parser.parse('/some/file.xtb', XTB);
expect(result.translations['8841459487341224498']).toEqual(ɵmakeParsedTranslation(['rab']));
@@ -60,7 +60,7 @@ describe('XtbTranslationParser', () => {
rab
`;
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
const result = parser.parse('/some/file.xtb', XTB);
expect(result.translations['8877975308926375834'])
@@ -73,7 +73,7 @@ describe('XtbTranslationParser', () => {
* *
{VAR_PLURAL, plural, =1 { rab }}
`;
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
const result = parser.parse('/some/file.xtb', XTB);
expect(result.translations['7717087045075616176'])
@@ -90,7 +90,7 @@ describe('XtbTranslationParser', () => {
toto
tata
`;
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
const result = parser.parse('/some/file.xtb', XTB);
expect(result.translations[ɵcomputeMsgId('foo')]).toEqual(ɵmakeParsedTranslation(['oof']));
@@ -103,7 +103,7 @@ describe('XtbTranslationParser', () => {
`;
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
const result = parser.parse('/some/file.xtb', XTB);
expect(result.translations[ɵcomputeMsgId('{$LINE_BREAK}{$TAG_IMG}{$TAG_IMG_1}')])
@@ -123,7 +123,7 @@ describe('XtbTranslationParser', () => {
`;
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
const result = parser.parse('/some/file.xtb', XTB);
expect(result.translations[ɵcomputeMsgId('hello {$START_TAG_SPAN}{$CLOSE_TAG_SPAN}')])
@@ -150,7 +150,7 @@ describe('XtbTranslationParser', () => {
Le test:
{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other { profondément imbriqué }}} =other {beaucoup}}
`;
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
const result = parser.parse('/some/file.xtb', XTB);
expect(result.translations[ɵcomputeMsgId('Test: {$ICU}')])
@@ -177,7 +177,7 @@ describe('XtbTranslationParser', () => {
multi\nlignes
`;
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
const result = parser.parse('/some/file.xtb', XTB);
expect(result.translations[ɵcomputeMsgId('multi\nlines')])
@@ -194,8 +194,7 @@ describe('XtbTranslationParser', () => {
`;
// Parsing the file should not fail
- const diagnostics = new Diagnostics();
- const parser = new XtbTranslationParser(diagnostics);
+ const parser = new XtbTranslationParser();
const result = parser.parse('/some/file.xtb', XTB);
// We should be able to read the valid message
@@ -204,7 +203,7 @@ describe('XtbTranslationParser', () => {
// Trying to access the invalid message should fail
expect(result.translations['invalid']).toBeUndefined();
- expect(diagnostics.messages).toContain({
+ expect(result.diagnostics.messages).toContain({
type: 'warning',
message:
`Could not parse message with id "invalid" - perhaps it has an unrecognised ICU format?\n` +
@@ -219,9 +218,11 @@ describe('XtbTranslationParser', () => {
' ';
expect(() => {
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
parser.parse('/some/file.xtb', XTB);
- }).toThrowError(/ elements can not be nested/);
+ }).toThrowError(`Failed to parse "/some/file.xtb" as XMB/XTB format
+ERRORS:
+ - Unexpected tag. ("[ERROR ->] "): /some/file.xtb@0:19`);
});
it('should throw when a translation has no id attribute', () => {
@@ -231,7 +232,7 @@ describe('XtbTranslationParser', () => {
`;
expect(() => {
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
parser.parse('/some/file.xtb', XTB);
}).toThrowError(/Missing required "id" attribute/);
});
@@ -244,7 +245,7 @@ describe('XtbTranslationParser', () => {
`;
expect(() => {
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
parser.parse('/some/file.xtb', XTB);
}).toThrowError(/Duplicated translations for message "deadbeef"/);
});
@@ -260,7 +261,7 @@ describe('XtbTranslationParser', () => {
`;
expect(() => {
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
parser.parse('/some/file.xtb', XTB);
}).toThrowError(/Invalid element found in message/);
});
@@ -272,10 +273,344 @@ describe('XtbTranslationParser', () => {
`;
expect(() => {
- const parser = new XtbTranslationParser(new Diagnostics());
+ const parser = new XtbTranslationParser();
parser.parse('/some/file.xtb', XTB);
}).toThrowError(/required "name" attribute/gi);
});
});
});
+
+ describe('parse() [with hint]', () => {
+ it('should extract the locale from the file contents', () => {
+ const XTB = `
+
+ rab
+ `;
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+ expect(result.locale).toEqual('fr');
+ });
+
+ it('should extract basic messages', () => {
+ const XTB = `
+
+
+
+
+
+
+
+
+ ]>
+
+ rab
+ `;
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+
+ expect(result.translations['8841459487341224498']).toEqual(ɵmakeParsedTranslation(['rab']));
+ });
+
+ it('should extract translations with simple placeholders', () => {
+ const XTB = `
+
+ rab
+ `;
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+
+ expect(result.translations['8877975308926375834'])
+ .toEqual(ɵmakeParsedTranslation(['', 'rab', ''], ['START_PARAGRAPH', 'CLOSE_PARAGRAPH']));
+ });
+
+ it('should extract translations with simple ICU expressions', () => {
+ const XTB = `
+
+ * *
+ {VAR_PLURAL, plural, =1 { rab }}
+ `;
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+
+ expect(result.translations['7717087045075616176'])
+ .toEqual(ɵmakeParsedTranslation(['*', '*'], ['ICU']));
+ expect(result.translations['5115002811911870583'])
+ .toEqual(ɵmakeParsedTranslation(
+ ['{VAR_PLURAL, plural, =1 {{START_PARAGRAPH}rab{CLOSE_PARAGRAPH}}}'], []));
+ });
+
+ it('should extract translations with duplicate source messages', () => {
+ const XTB = `
+
+ oof
+ toto
+ tata
+ `;
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+
+ expect(result.translations[ɵcomputeMsgId('foo')]).toEqual(ɵmakeParsedTranslation(['oof']));
+ expect(result.translations['i']).toEqual(ɵmakeParsedTranslation(['toto']));
+ expect(result.translations['bar']).toEqual(ɵmakeParsedTranslation(['tata']));
+ });
+
+ it('should extract translations with only placeholders, which are re-ordered', () => {
+ const XTB = `
+
+
+ `;
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+
+ expect(result.translations[ɵcomputeMsgId('{$LINE_BREAK}{$TAG_IMG}{$TAG_IMG_1}')])
+ .toEqual(
+ ɵmakeParsedTranslation(['', '', '', ''], ['TAG_IMG_1', 'TAG_IMG', 'LINE_BREAK']));
+ });
+
+ it('should extract translations with empty target', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * hello
+ * ```
+ */
+ const XTB = `
+
+
+ `;
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+
+ expect(result.translations[ɵcomputeMsgId('hello {$START_TAG_SPAN}{$CLOSE_TAG_SPAN}')])
+ .toEqual(ɵmakeParsedTranslation(['']));
+ });
+
+ it('should extract translations with deeply nested ICUs', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * Test: { count, plural, =0 { { sex, select, other {deeply nested
}} } =other {a lot}}
+ * ```
+ *
+ * Note that the message gets split into two translation units:
+ * * The first one contains the outer message with an `ICU` placeholder
+ * * The second one is the ICU expansion itself
+ *
+ * Note that special markers `VAR_PLURAL` and `VAR_SELECT` are added, which are then
+ replaced by IVY at runtime with the actual values being rendered by the ICU expansion.
+ */
+ const XTB = `
+
+ Le test:
+ {VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other { profondément imbriqué }}} =other {beaucoup}}
+ `;
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+
+ expect(result.translations[ɵcomputeMsgId('Test: {$ICU}')])
+ .toEqual(ɵmakeParsedTranslation(['Le test: ', ''], ['ICU']));
+
+ expect(
+ result.translations[ɵcomputeMsgId(
+ '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}deeply nested{CLOSE_PARAGRAPH}}}} =other {beaucoup}}')])
+ .toEqual(ɵmakeParsedTranslation([
+ '{VAR_PLURAL, plural, =0 {{VAR_SELECT, select, other {{START_PARAGRAPH}profondément imbriqué{CLOSE_PARAGRAPH}}}} =other {beaucoup}}'
+ ]));
+ });
+
+ it('should extract translations containing multiple lines', () => {
+ /**
+ * Source HTML:
+ *
+ * ```
+ * multi
+ * lines
+ * ```
+ */
+ const XTB = `
+
+ multi\nlignes
+ `;
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+
+ expect(result.translations[ɵcomputeMsgId('multi\nlines')])
+ .toEqual(ɵmakeParsedTranslation(['multi\nlignes']));
+ });
+
+ it('should warn on unrecognised ICU messages', () => {
+ // See https://github.com/angular/angular/issues/14046
+
+ const XTB = `
+
+ This is a valid message
+ {REGION_COUNT_1, plural, =0 {unused plural form} =1 {1 region} other {{REGION_COUNT_2} regions}}
+ `;
+
+ // Parsing the file should not fail
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+
+ // We should be able to read the valid message
+ expect(result.translations['valid'])
+ .toEqual(ɵmakeParsedTranslation(['This is a valid message']));
+
+ // Trying to access the invalid message should fail
+ expect(result.translations['invalid']).toBeUndefined();
+ expect(result.diagnostics.messages).toContain({
+ type: 'warning',
+ message:
+ `Could not parse message with id "invalid" - perhaps it has an unrecognised ICU format?\n` +
+ `Error: Unexpected character "EOF" (Do you have an unescaped "{" in your template? Use "{{ '{' }}") to escape it.)\n` +
+ `Error: Invalid ICU message. Missing '}'.`
+ });
+ });
+
+ describe('[structure errors]', () => {
+ it('should throw when there are nested translationbundle tags', () => {
+ const XTB =
+ ' ';
+
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message)
+ .toEqual(
+ `Unexpected tag. ("[ERROR ->] "): /some/file.xtb@0:19`);
+ });
+
+ it('should throw when a translation has no id attribute', () => {
+ const XTB = `
+
+
+ `;
+
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message)
+ .toEqual(`Missing required "id" attribute on element. ("
+
+ [ERROR ->]
+ "): /some/file.xtb@2:12`);
+ });
+
+ it('should throw on duplicate translation id', () => {
+ const XTB = `
+
+
+
+ `;
+
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message)
+ .toEqual(`Duplicated translations for message "deadbeef" ("
+
+
+ [ERROR ->]
+ "): /some/file.xtb@3:12`);
+ });
+ });
+
+ describe('[message errors]', () => {
+ it('should throw on unknown message tags', () => {
+ const XTB = `
+
+
+
+
+ `;
+
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message).toEqual(`Invalid element found in message. ("
+
+
+ [ERROR ->]
+
+ "): /some/file.xtb@3:14`);
+ });
+
+ it('should throw when a placeholder misses a name attribute', () => {
+ const XTB = `
+
+
+ `;
+
+ const parser = new XtbTranslationParser();
+ const hint = parser.canParse('/some/file.xtb', XTB);
+ if (!hint) {
+ return fail('expected XTB to be valid');
+ }
+ const result = parser.parse('/some/file.xtb', XTB, hint);
+ expect(result.diagnostics.messages.length).toEqual(1);
+ expect(result.diagnostics.messages[0].message)
+ .toEqual(`Missing required "name" attribute: ("
+
+ [ERROR ->]
+ "): /some/file.xtb@2:39`);
+ });
+ });
+ });
});
diff --git a/packages/platform-browser/package.json b/packages/platform-browser/package.json
index f20cfdd2c183..6a1b676555b1 100644
--- a/packages/platform-browser/package.json
+++ b/packages/platform-browser/package.json
@@ -13,10 +13,16 @@
"author": "angular",
"license": "MIT",
"peerDependencies": {
+ "@angular/animations": "0.0.0-PLACEHOLDER",
"@angular/core": "0.0.0-PLACEHOLDER",
"@angular/common": "0.0.0-PLACEHOLDER",
"tslib": "^1.10.0"
},
+ "peerDependenciesMeta": {
+ "@angular/animations": {
+ "optional": true
+ }
+ },
"repository": {
"type": "git",
"url": "https://github.com/angular/angular.git",
diff --git a/packages/router/src/directives/router_link.ts b/packages/router/src/directives/router_link.ts
index c12ca2c8cf91..15107556ba4c 100644
--- a/packages/router/src/directives/router_link.ts
+++ b/packages/router/src/directives/router_link.ts
@@ -163,6 +163,7 @@ export class RouterLink {
const extras = {
skipLocationChange: attrBoolValue(this.skipLocationChange),
replaceUrl: attrBoolValue(this.replaceUrl),
+ state: this.state
};
this.router.navigateByUrl(this.urlTree, extras);
return true;
diff --git a/packages/router/test/BUILD.bazel b/packages/router/test/BUILD.bazel
index 9c051775bec3..bbadc0ccba9c 100644
--- a/packages/router/test/BUILD.bazel
+++ b/packages/router/test/BUILD.bazel
@@ -44,21 +44,6 @@ jasmine_node_test(
karma_web_test_suite(
name = "test_web",
- tags = [
- # FIXME: fix on saucelabs
- # IE 11.0.0 (Windows 8.1.0.0) bootstrap should restore the scrolling position FAILED
- # IE 10.0.0 (Windows 8.0.0) bootstrap should restore the scrolling position FAILED
- # Error: Expected undefined to equal 5000.
- # at
- # Error: Expected undefined to equal 3000.
- # at
- # Error: Expected undefined to equal 0.
- # at
- # Error: Expected false to be true.
- # at
- "fixme-saucelabs-ivy",
- "fixme-saucelabs-ve",
- ],
deps = [
":test_lib",
],
diff --git a/packages/router/test/bootstrap.spec.ts b/packages/router/test/bootstrap.spec.ts
index a409fe90a26c..19c3bb69d8b0 100644
--- a/packages/router/test/bootstrap.spec.ts
+++ b/packages/router/test/bootstrap.spec.ts
@@ -298,24 +298,27 @@ describe('bootstrap', () => {
await router.navigateByUrl('/aa');
window.scrollTo(0, 5000);
+ // IE 9/10/11 use non-standard pageYOffset instead of scrollY
+ const getScrollY = () => window.scrollY !== undefined ? window.scrollY : window.pageYOffset;
+
await router.navigateByUrl('/fail');
- expect(window.scrollY).toEqual(5000);
+ expect(getScrollY()).toEqual(5000);
await router.navigateByUrl('/bb');
window.scrollTo(0, 3000);
- expect(window.scrollY).toEqual(3000);
+ expect(getScrollY()).toEqual(3000);
await router.navigateByUrl('/cc');
- expect(window.scrollY).toEqual(0);
+ expect(getScrollY()).toEqual(0);
await router.navigateByUrl('/aa#marker2');
- expect(window.scrollY >= 5900).toBe(true);
+ expect(getScrollY() >= 5900).toBe(true);
expect(window.scrollY < 6000).toBe(true); // offset
await router.navigateByUrl('/aa#marker3');
- expect(window.scrollY >= 8900).toBe(true);
- expect(window.scrollY < 9000).toBe(true);
+ expect(getScrollY() >= 8900).toBe(true);
+ expect(getScrollY() < 9000).toBe(true);
done();
});
diff --git a/packages/zone.js/lib/zone-spec/fake-async-test.ts b/packages/zone.js/lib/zone-spec/fake-async-test.ts
index 625b9a776355..661dc08e3999 100644
--- a/packages/zone.js/lib/zone-spec/fake-async-test.ts
+++ b/packages/zone.js/lib/zone-spec/fake-async-test.ts
@@ -136,11 +136,12 @@
}
}
- tick(millis: number = 0, doTick?: (elapsed: number) => void, tickOptions: {
+ tick(millis: number = 0, doTick?: (elapsed: number) => void, tickOptions?: {
processNewMacroTasksSynchronously: boolean
- } = {processNewMacroTasksSynchronously: true}): void {
+ }): void {
let finalTime = this._currentTime + millis;
let lastCurrentTime = 0;
+ tickOptions = Object.assign({processNewMacroTasksSynchronously: true}, tickOptions);
// we need to copy the schedulerQueue so nested timeout
// will not be wrongly called in the current tick
// https://github.com/angular/angular/issues/33799
diff --git a/packages/zone.js/test/zone-spec/fake-async-test.spec.ts b/packages/zone.js/test/zone-spec/fake-async-test.spec.ts
index 6c7b82ed689b..eae4658cb981 100644
--- a/packages/zone.js/test/zone-spec/fake-async-test.spec.ts
+++ b/packages/zone.js/test/zone-spec/fake-async-test.spec.ts
@@ -145,6 +145,20 @@ describe('FakeAsyncTestZoneSpec', () => {
});
}));
+ it('should default to processNewMacroTasksSynchronously if providing other flags', () => {
+ function nestedTimer(callback: () => any): void {
+ setTimeout(() => setTimeout(() => callback()));
+ }
+ fakeAsyncTestZone.run(() => {
+ const callback = jasmine.createSpy('callback');
+ nestedTimer(callback);
+ expect(callback).not.toHaveBeenCalled();
+ testZoneSpec.tick(0, null, {});
+ expect(callback).toHaveBeenCalled();
+ });
+ });
+
+
it('should not queue new macro task on tick with processNewMacroTasksSynchronously=false',
() => {
function nestedTimer(callback: () => any): void {
diff --git a/scripts/build/build-ivy-npm-packages.js b/scripts/build/build-ivy-npm-packages.js
index 04bdec277686..77447dfaad13 100755
--- a/scripts/build/build-ivy-npm-packages.js
+++ b/scripts/build/build-ivy-npm-packages.js
@@ -9,8 +9,18 @@
'use strict';
+const {buildZoneJsPackage} = require('./zone-js-builder');
const {buildTargetPackages} = require('./package-builder');
// Build the ivy packages into `dist/packages-dist-ivy-aot/`.
buildTargetPackages('dist/packages-dist-ivy-aot', true, 'Ivy AOT');
+
+// Build the `zone.js` npm package into `dist/zone.js-dist-ivy-aot/`, because it might be needed by
+// other scripts/tests.
+//
+// NOTE:
+// The `-ivy-aot` suffix is only used to differentiate from the packages built by the
+// `build-packages-dist.js` script, so that there is no conflict when persisting them to the
+// workspace on CI.
+buildZoneJsPackage('dist/zone.js-dist-ivy-aot');
diff --git a/scripts/build/build-packages-dist.js b/scripts/build/build-packages-dist.js
index 446add09b53a..3e7c9bd5bdf1 100755
--- a/scripts/build/build-packages-dist.js
+++ b/scripts/build/build-packages-dist.js
@@ -10,6 +10,7 @@
'use strict';
const {buildZoneJsPackage} = require('./zone-js-builder');
+const {buildDevInfraPackage} = require('./dev-infra-builder');
const {buildTargetPackages} = require('./package-builder');
@@ -18,4 +19,7 @@ buildTargetPackages('dist/packages-dist', false, 'Production');
// Build the `zone.js` npm package into `dist/zone.js-dist/`, because it might be needed by other
// scripts/tests.
-buildZoneJsPackage();
+buildZoneJsPackage('dist/zone.js-dist');
+
+// Build the `angular-dev-infra` npm package into `dist/packages-dist/@angular/dev-infra-private`
+buildDevInfraPackage();
diff --git a/scripts/build/dev-infra-builder.js b/scripts/build/dev-infra-builder.js
new file mode 100644
index 000000000000..43c672b09fc0
--- /dev/null
+++ b/scripts/build/dev-infra-builder.js
@@ -0,0 +1,38 @@
+/**
+ * @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
+ */
+
+'use strict';
+
+const {chmod, cp, mkdir, rm} = require('shelljs');
+const {baseDir, bazelBin, bazelCmd, exec, scriptPath} = require('./package-builder');
+
+/**
+ * Build the `@angular/dev-infra-private` npm package and copies it to `dist/packages-dist`.
+ */
+function buildDevInfraPackage() {
+ console.info('##############################');
+ console.info(`${scriptPath}:`);
+ console.info(' Building @angular/dev-infra-private npm package');
+ console.info('##############################');
+ exec(`${bazelCmd} build //dev-infra:npm_package`);
+
+ const buildOutputDir = `${bazelBin}/dev-infra/npm_package`;
+ const distTargetDir = `${baseDir}/dist/packages-dist/dev-infra-private`;
+
+ console.info(`# Copy artifacts to ${distTargetDir}`);
+ mkdir('-p', distTargetDir);
+ rm('-rf', distTargetDir);
+ cp('-R', buildOutputDir, distTargetDir);
+ chmod('-R', 'u+w', distTargetDir);
+
+ console.info('');
+}
+
+module.exports = {
+ buildDevInfraPackage
+};
diff --git a/scripts/build/package-builder.js b/scripts/build/package-builder.js
index b5c2d5335007..f1b153bc2107 100644
--- a/scripts/build/package-builder.js
+++ b/scripts/build/package-builder.js
@@ -51,7 +51,7 @@ module.exports = {
};
/**
- * Build the packages.
+ * Build the Angular packages.
*
* @param {string} destPath Path to the output directory into which we copy the npm packages.
* This path should either be absolute or relative to the project root.
diff --git a/scripts/build/zone-js-builder.js b/scripts/build/zone-js-builder.js
index 59cb8de31ff8..d43489df37de 100644
--- a/scripts/build/zone-js-builder.js
+++ b/scripts/build/zone-js-builder.js
@@ -8,7 +8,8 @@
'use strict';
-const {chmod, cp, mkdir, rm} = require('shelljs');
+const {resolve} = require('path');
+const {chmod, cp, mkdir, rm, test} = require('shelljs');
const {baseDir, bazelBin, bazelCmd, exec, scriptPath} = require('./package-builder');
@@ -18,27 +19,33 @@ module.exports = {
/**
* Build the `zone.js` npm package into `dist/bin/packages/zone.js/npm_package/` and copy it to
- * `dist/zone.js-dist/` for other scripts/tests to use.
+ * `destPath` for other scripts/tests to use.
*
* NOTE: The `zone.js` package is not built as part of `package-builder`'s `buildTargetPackages()`
* nor is it copied into the same directory as the Angular packages (e.g.
* `dist/packages-dist/`) despite its source's being inside `packages/`, because it is not
* published to npm under the `@angular` scope (as happens for the rest of the packages).
+ *
+ * @param {string} destPath Path to the output directory into which we copy the npm package.
+ * This path should either be absolute or relative to the project root.
*/
-function buildZoneJsPackage() {
+function buildZoneJsPackage(destPath) {
console.info('##############################');
console.info(`${scriptPath}:`);
console.info(' Building zone.js npm package');
console.info('##############################');
exec(`${bazelCmd} build //packages/zone.js:npm_package`);
- // Copy artifacts to `dist/zone.js-dist/`, so they can be easier persisted on CI and used by
- // non-bazel scripts/tests.
+ // Create the output directory.
+ const absDestPath = resolve(baseDir, destPath);
+ if (!test('-d', absDestPath)) mkdir('-p', absDestPath);
+
+ // Copy artifacts to `destPath`, so they can be easier persisted on CI and used by non-bazel
+ // scripts/tests.
const buildOutputDir = `${bazelBin}/packages/zone.js/npm_package`;
- const distTargetDir = `${baseDir}/dist/zone.js-dist/zone.js`;
+ const distTargetDir = `${absDestPath}/zone.js`;
console.info(`# Copy artifacts to ${distTargetDir}`);
- mkdir('-p', distTargetDir);
rm('-rf', distTargetDir);
cp('-R', buildOutputDir, distTargetDir);
chmod('-R', 'u+w', distTargetDir);
diff --git a/scripts/compare-master-to-patch.js b/scripts/compare-master-to-patch.js
new file mode 100755
index 000000000000..f5849dff10e3
--- /dev/null
+++ b/scripts/compare-master-to-patch.js
@@ -0,0 +1,147 @@
+#!/usr/bin/env node
+
+/**
+ * @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
+ */
+
+'use strict';
+
+/**
+ * This script compares commits in master and patch branches to find the delta between them. This is
+ * useful for release reviews, to make sure all the necessary commits were included into the patch
+ * branch and there is no discrepancy.
+ */
+
+const {exec} = require('shelljs');
+const semver = require('semver');
+
+// Ignore commits that have specific patterns in commit message, it's ok for these commits to be
+// present only in one branch. Ignoring them reduced the "noise" in the final output.
+const ignorePatterns = [
+ 'release:',
+ 'docs: release notes',
+ // These commits are created to update cli command docs sources with the most recent sha (stored
+ // in `aio/package.json`). Separate commits are generated for master and patch branches and since
+ // it's purely an infrastructure-related change, we ignore these commits while comparing master
+ // and patch diffs to look for delta.
+ 'build(docs-infra): upgrade cli command docs sources',
+];
+
+// Limit the log history to start from v9.0.0 release date.
+// Note: this is needed only for 9.0.x branch to avoid RC history.
+// Remove it once `9.1.x` branch is created.
+const after = '--after="2020-02-05"';
+
+// Helper methods
+
+function execGitCommand(gitCommand) {
+ const output = exec(gitCommand, {silent: true});
+ if (output.code !== 0) {
+ console.error(`Error: git command "${gitCommand}" failed: \n\n ${output.stderr}`);
+ process.exit(1);
+ }
+ return output;
+}
+
+function toArray(rawGitCommandOutput) {
+ return rawGitCommandOutput.trim().split('\n');
+}
+
+function maybeExtractReleaseVersion(commit) {
+ const versionRegex = /release: cut the (.*?) release|docs: release notes for the (.*?) release/;
+ const matches = commit.match(versionRegex);
+ return matches ? matches[1] || matches[2] : null;
+}
+
+function collectCommitsAsMap(rawGitCommits) {
+ const commits = toArray(rawGitCommits);
+ const commitsMap = new Map();
+ let version = 'initial';
+ commits.reverse().forEach((item) => {
+ const skip = ignorePatterns.some(pattern => item.indexOf(pattern) > -1);
+ // Keep track of the current version while going though the list of commits, so that we can use
+ // this information in the output (i.e. display a version when a commit was introduced).
+ version = maybeExtractReleaseVersion(item) || version;
+ if (!skip) {
+ // Extract original commit description from commit message, so that we can find matching
+ // commit in other commit range. For example, for the following commit message:
+ //
+ // 15d3e741e9 feat: update the locale files (#33556)
+ //
+ // we extract only "feat: update the locale files" part and use it as a key, since commit SHA
+ // and PR number may be different for the same commit in master and patch branches.
+ const key = item.slice(11).replace(/\(\#\d+\)/g, '').trim();
+ commitsMap.set(key, [item, version]);
+ }
+ });
+ return commitsMap;
+}
+
+/**
+ * Returns a list of items present in `mapA`, but *not* present in `mapB`.
+ * This function is needed to compare 2 sets of commits and return the list of unique commits in the
+ * first set.
+ */
+function diff(mapA, mapB) {
+ const result = [];
+ mapA.forEach((value, key) => {
+ if (!mapB.has(key)) {
+ result.push(`[${value[1]}+] ${value[0]}`);
+ }
+ });
+ return result;
+}
+
+function getBranchByTag(tag) {
+ const version = semver(tag);
+ return `${version.major}.${version.minor}.x`; // e.g. 9.0.x
+}
+
+function getLatestTag(tags) {
+ // Exclude Next releases, since we cut them from master, so there is nothing to compare.
+ const isNotNextVersion = version => version.indexOf('-next') === -1;
+ return tags.filter(semver.valid)
+ .filter(isNotNextVersion)
+ .map(semver.clean)
+ .sort(semver.rcompare)[0];
+}
+
+// Main program
+function main() {
+ execGitCommand('git fetch upstream');
+
+ // Extract tags information and pick the most recent version
+ // that we'll use later to compare with master.
+ const tags = toArray(execGitCommand('git tag'));
+ const latestTag = getLatestTag(tags);
+
+ // Based on the latest tag, generate the name of the patch branch.
+ const branch = getBranchByTag(latestTag);
+
+ // Extract master-only and patch-only commits using `git log` command.
+ const masterCommits = execGitCommand(
+ `git log --cherry-pick --oneline --right-only ${after} upstream/${branch}...upstream/master`);
+ const patchCommits = execGitCommand(
+ `git log --cherry-pick --oneline --left-only ${after} upstream/${branch}...upstream/master`);
+
+ // Post-process commits and convert raw data into a Map, so that we can diff it easier.
+ const masterCommitsMap = collectCommitsAsMap(masterCommits);
+ const patchCommitsMap = collectCommitsAsMap(patchCommits);
+
+ // tslint:disable-next-line:no-console
+ console.log(`
+Comparing branches "${branch}" and master.
+
+***** Only in MASTER *****
+${diff(masterCommitsMap, patchCommitsMap).join('\n') || 'No extra commits'}
+
+***** Only in PATCH (${branch}) *****
+${diff(patchCommitsMap, masterCommitsMap).join('\n') || 'No extra commits'}
+`);
+}
+
+main();
\ No newline at end of file
diff --git a/tools/contributing-stats/get-data.ts b/tools/contributing-stats/get-data.ts
new file mode 100644
index 000000000000..c4cf31028bdf
--- /dev/null
+++ b/tools/contributing-stats/get-data.ts
@@ -0,0 +1,244 @@
+/**
+ * @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
+ */
+
+/**
+ * This script gets contribution stats for all members of the angular org,
+ * since a provided date.
+ * The script expects the following flag(s):
+ *
+ * required:
+ * --since [date] The data after which contributions are queried for.
+ * Uses githubs search format for dates, e.g. "2020-01-21".
+ * See
+ * https://help.github.com/en/github/searching-for-information-on-github/understanding-the-search-syntax#query-for-dates
+ *
+ * optional:
+ * --use-created [boolean] If the created timestamp should be used for
+ * time comparisons, defaults otherwise to the updated timestamp.
+ */
+
+import {graphql as unauthenticatedGraphql} from '@octokit/graphql';
+import * as minimist from 'minimist';
+import {alias, params, query as graphqlQuery, types} from 'typed-graphqlify';
+
+// The organization to be considered for the queries.
+const ORG = 'angular';
+// The repositories to be considered for the queries.
+const REPOS = ['angular', 'components', 'angular-cli'];
+
+/**
+ * Handle flags for the script.
+ */
+const args = minimist(process.argv.slice(2), {
+ string: ['since'],
+ boolean: ['use-created'],
+ unknown: (option: string) => {
+ console.error(`Unknown option: ${option}`);
+ process.exit(1);
+ }
+});
+
+if (!args['since']) {
+ console.error(`Please provide --since [date]`);
+ process.exit(1);
+}
+
+/**
+ * Authenticated instance of Github GraphQl API service, relies on a
+ * personal access token being available in the TOKEN environment variable.
+ */
+const graphql = unauthenticatedGraphql.defaults({
+ headers: {
+ // TODO(josephperrott): Remove reference to TOKEN environment variable as part of larger
+ // effort to migrate to expecting tokens via GITHUB_ACCESS_TOKEN environment variables.
+ authorization: `token ${process.env.TOKEN || process.env.GITHUB_ACCESS_TOKEN}`,
+ }
+});
+
+/**
+ * Retrieves all current members of an organization.
+ */
+async function getAllOrgMembers() {
+ // The GraphQL query object to get a page of members of an organization.
+ const MEMBERS_QUERY = params(
+ {
+ $first: 'Int', // How many entries to get with each request
+ $after: 'String', // The cursor to start the page at
+ $owner: 'String!', // The organization to query for
+ },
+ {
+ organization: params({login: '$owner'}, {
+ membersWithRole: params(
+ {
+ first: '$first',
+ after: '$after',
+ },
+ {
+ nodes: [{login: types.string}],
+ pageInfo: {
+ hasNextPage: types.boolean,
+ endCursor: types.string,
+ },
+ }),
+ })
+ });
+ const query = graphqlQuery('members', MEMBERS_QUERY);
+
+ /**
+ * Gets the query and queryParams for a specific page of entries.
+ */
+ const queryBuilder = (count: number, cursor?: string) => {
+ return {
+ query,
+ params: {
+ after: cursor || null,
+ first: count,
+ owner: ORG,
+ },
+ };
+ };
+
+ // The current cursor
+ let cursor = undefined;
+ // If an additional page of members is expected
+ let hasNextPage = true;
+ // Array of Github usernames of the organization
+ const members: string[] = [];
+
+ while (hasNextPage) {
+ const {query, params} = queryBuilder(100, cursor);
+ const results = await graphql(query, params) as typeof MEMBERS_QUERY;
+
+ results.organization.membersWithRole.nodes.forEach(
+ (node: {login: string}) => members.push(node.login));
+ hasNextPage = results.organization.membersWithRole.pageInfo.hasNextPage;
+ cursor = results.organization.membersWithRole.pageInfo.endCursor;
+ }
+ return members;
+}
+
+/**
+ * Build metadata for making requests for a specific user and date.
+ *
+ * Builds GraphQL query string, Query Params and Labels for making queries to GraphQl.
+ */
+function buildQueryAndParams(username: string, date: string) {
+ // Whether the updated or created timestamp should be used.
+ const updatedOrCreated = args['use-created'] ? 'created' : 'updated';
+ let dataQueries: {[key: string]: {query: string, label: string}} = {};
+ // Add queries and params for all values queried for each repo.
+ for (let repo of REPOS) {
+ dataQueries = {
+ ...dataQueries,
+ [`${repo.replace(/[\/\-]/g, '_')}_issue_author`]: {
+ query: `repo:${ORG}/${repo} is:issue author:${username} ${updatedOrCreated}:>${date}`,
+ label: `${ORG}/${repo} Issue Authored`,
+ },
+ [`${repo.replace(/[\/\-]/g, '_')}_issues_involved`]: {
+ query:
+ `repo:${ORG}/${repo} is:issue -author:${username} involves:${username} ${updatedOrCreated}:>${date}`,
+ label: `${ORG}/${repo} Issue Involved`,
+ },
+ [`${repo.replace(/[\/\-]/g, '_')}_pr_author`]: {
+ query: `repo:${ORG}/${repo} is:pr author:${username} ${updatedOrCreated}:>${date}`,
+ label: `${ORG}/${repo} PR Author`,
+ },
+ [`${repo.replace(/[\/\-]/g, '_')}_pr_involved`]: {
+ query: `repo:${ORG}/${repo} is:pr involves:${username} ${updatedOrCreated}:>${date}`,
+ label: `${ORG}/${repo} PR Involved`,
+ },
+ [`${repo.replace(/[\/\-]/g, '_')}_pr_reviewed`]: {
+ query:
+ `repo:${ORG}/${repo} is:pr -author:${username} reviewed-by:${username} ${updatedOrCreated}:>${date}`,
+ label: `${ORG}/${repo} PR Reviewed`,
+ },
+ [`${repo.replace(/[\/\-]/g, '_')}_pr_commented`]: {
+ query:
+ `repo:${ORG}/${repo} is:pr -author:${username} commenter:${username} ${updatedOrCreated}:>${date}`,
+ label: `${ORG}/${repo} PR Commented`,
+ },
+ };
+ }
+ // Add queries and params for all values queried for the org.
+ dataQueries = {
+ ...dataQueries,
+ [`${ORG}_org_issue_author`]: {
+ query: `org:${ORG} is:issue author:${username} ${updatedOrCreated}:>${date}`,
+ label: `${ORG} org Issue Authored`,
+ },
+ [`${ORG}_org_issues_involved`]: {
+ query:
+ `org:${ORG} is:issue -author:${username} involves:${username} ${updatedOrCreated}:>${date}`,
+ label: `${ORG} org Issue Involved`,
+ },
+ [`${ORG}_org_pr_author`]: {
+ query: `org:${ORG} is:pr author:${username} ${updatedOrCreated}:>${date}`,
+ label: `${ORG} org PR Author`,
+ },
+ [`${ORG}_org_pr_involved`]: {
+ query: `org:${ORG} is:pr involves:${username} ${updatedOrCreated}:>${date}`,
+ label: `${ORG} org PR Involved`,
+ },
+ [`${ORG}_org_pr_reviewed`]: {
+ query:
+ `org:${ORG} is:pr -author:${username} reviewed-by:${username} ${updatedOrCreated}:>${date}`,
+ label: `${ORG} org PR Reviewed`,
+ },
+ [`${ORG}_org_pr_commented`]: {
+ query:
+ `org:${ORG} is:pr -author:${username} commenter:${username} ${updatedOrCreated}:>${date}`,
+ label: `${ORG} org PR Commented`,
+ },
+ };
+
+ /**
+ * Gets the labels for each requested value to be used as headers.
+ */
+ function getLabels(pairs: typeof dataQueries) {
+ return Object.values(pairs).map(val => val.label);
+ }
+
+ /**
+ * Gets the graphql query object for the GraphQL query.
+ */
+ function getQuery(pairs: typeof dataQueries) {
+ const output: {[key: string]: {}} = {};
+ Object.entries(pairs).map(([key, val]) => {
+ output[alias(key, 'search')] = params(
+ {
+ query: `"${val.query}"`,
+ type: 'ISSUE',
+ },
+ {
+ issueCount: types.number,
+ });
+ });
+ return output;
+ }
+
+ return {
+ query: graphqlQuery(getQuery(dataQueries)),
+ labels: getLabels(dataQueries),
+ };
+}
+
+/**
+ * Runs the script to create a CSV string with the requested data for each member
+ * of the organization.
+ */
+async function run(date: string) {
+ console.info(['Username'].concat(buildQueryAndParams('', date).labels).join(','));
+
+ for (let username of await getAllOrgMembers()) {
+ const results = await graphql(buildQueryAndParams(username, date).query);
+ const values = Object.values(results).map(result => `${result.issueCount}`);
+ console.info([username].concat(values).join(','));
+ }
+}
+
+run(args['since']);
diff --git a/tools/gulp-tasks/changelog.js b/tools/gulp-tasks/changelog.js
index 432ba3d9861b..c9c3c9eba69f 100644
--- a/tools/gulp-tasks/changelog.js
+++ b/tools/gulp-tasks/changelog.js
@@ -10,6 +10,7 @@ module.exports = (gulp) => () => {
const conventionalChangelog = require('gulp-conventional-changelog');
const ignoredScopes = [
'aio',
+ 'dev-infra',
'docs-infra',
'zone.js',
];
diff --git a/tools/public_api_guard/BUILD.bazel b/tools/public_api_guard/BUILD.bazel
index 922324ebc7c7..81dc44d04413 100644
--- a/tools/public_api_guard/BUILD.bazel
+++ b/tools/public_api_guard/BUILD.bazel
@@ -66,7 +66,8 @@ ts_api_guardian_test(
golden = "angular/tools/public_api_guard/global_utils.d.ts",
)
-# explicit target because the d.ts file is nested in the core and not part of typical public d.ts api
+# explicit target because the d.ts file is nested in the compiler and not part of typical public
+# d.ts api
ts_api_guardian_test(
name = "error_code_api",
actual = "angular/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.d.ts",
@@ -76,3 +77,15 @@ ts_api_guardian_test(
],
golden = "angular/tools/public_api_guard/error_code.d.ts",
)
+
+# explicit target because the d.ts file is nested in the compiler and not part of typical public
+# d.ts api
+ts_api_guardian_test(
+ name = "compiler_options_api",
+ actual = "angular/packages/compiler-cli/src/ngtsc/core/api/src/public_options.d.ts",
+ data = [
+ ":compiler_options.d.ts",
+ "//packages/compiler-cli/src/ngtsc/core:api",
+ ],
+ golden = "angular/tools/public_api_guard/compiler_options.d.ts",
+)
diff --git a/tools/public_api_guard/compiler_options.d.ts b/tools/public_api_guard/compiler_options.d.ts
new file mode 100644
index 000000000000..b5ca8bf59939
--- /dev/null
+++ b/tools/public_api_guard/compiler_options.d.ts
@@ -0,0 +1,43 @@
+export interface BazelAndG3Options {
+ annotateForClosureCompiler?: boolean;
+ generateDeepReexports?: boolean;
+}
+
+export interface I18nOptions {
+ enableI18nLegacyMessageIdFormat?: boolean;
+ i18nInLocale?: string;
+ i18nUseExternalIds?: boolean;
+}
+
+export interface LegacyNgcOptions {
+ allowEmptyCodegenFiles?: boolean;
+ flatModuleId?: string;
+ flatModuleOutFile?: string;
+ fullTemplateTypeCheck?: boolean;
+ preserveWhitespaces?: boolean;
+ strictInjectionParameters?: boolean;
+}
+
+export interface MiscOptions {
+ compileNonExportedClasses?: boolean;
+ disableTypeScriptVersionCheck?: boolean;
+}
+
+export interface NgcCompatibilityOptions {
+ enableIvy?: boolean | 'ngtsc';
+ generateNgFactoryShims?: boolean;
+ generateNgSummaryShims?: boolean;
+}
+
+export interface StrictTemplateOptions {
+ strictAttributeTypes?: boolean;
+ strictContextGenerics?: boolean;
+ strictDomEventTypes?: boolean;
+ strictDomLocalRefTypes?: boolean;
+ strictInputTypes?: boolean;
+ strictLiteralTypes?: boolean;
+ strictNullInputTypes?: boolean;
+ strictOutputEventTypes?: boolean;
+ strictSafeNavigationTypes?: boolean;
+ strictTemplates?: boolean;
+}
diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts
index 2184ce1cf1e2..d927be91d16c 100644
--- a/tools/public_api_guard/core/core.d.ts
+++ b/tools/public_api_guard/core/core.d.ts
@@ -915,7 +915,7 @@ export declare function ɵɵnextContext(level?: number): T;
export declare type ɵɵNgModuleDefWithMeta = NgModuleDef;
-export declare function ɵɵNgOnChangesFeature(): DirectiveDefFeature;
+export declare function ɵɵNgOnChangesFeature(definition: DirectiveDef): void;
export declare function ɵɵpipe(index: number, pipeName: string): any;
diff --git a/tools/validate-commit-message/commit-message.json b/tools/validate-commit-message/commit-message.json
index 1e5c2bc448ae..09d8e70e0e6b 100644
--- a/tools/validate-commit-message/commit-message.json
+++ b/tools/validate-commit-message/commit-message.json
@@ -21,6 +21,7 @@
"compiler",
"compiler-cli",
"core",
+ "dev-infra",
"docs-infra",
"elements",
"forms",
diff --git a/yarn.lock b/yarn.lock
index 3ecebde2d24d..ee7f7a1afc17 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1273,6 +1273,54 @@
rxjs "6.5.3"
webpack-sources "1.4.3"
+"@octokit/endpoint@^5.5.0":
+ version "5.5.3"
+ resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-5.5.3.tgz#0397d1baaca687a4c8454ba424a627699d97c978"
+ integrity sha512-EzKwkwcxeegYYah5ukEeAI/gYRLv2Y9U5PpIsseGSFDk+G3RbipQGBs8GuYS1TLCtQaqoO66+aQGtITPalxsNQ==
+ dependencies:
+ "@octokit/types" "^2.0.0"
+ is-plain-object "^3.0.0"
+ universal-user-agent "^5.0.0"
+
+"@octokit/graphql@^4.3.1":
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.3.1.tgz#9ee840e04ed2906c7d6763807632de84cdecf418"
+ integrity sha512-hCdTjfvrK+ilU2keAdqNBWOk+gm1kai1ZcdjRfB30oA3/T6n53UVJb7w0L5cR3/rhU91xT3HSqCd+qbvH06yxA==
+ dependencies:
+ "@octokit/request" "^5.3.0"
+ "@octokit/types" "^2.0.0"
+ universal-user-agent "^4.0.0"
+
+"@octokit/request-error@^1.0.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801"
+ integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA==
+ dependencies:
+ "@octokit/types" "^2.0.0"
+ deprecation "^2.0.0"
+ once "^1.4.0"
+
+"@octokit/request@^5.3.0":
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.3.2.tgz#1ca8b90a407772a1ee1ab758e7e0aced213b9883"
+ integrity sha512-7NPJpg19wVQy1cs2xqXjjRq/RmtSomja/VSWnptfYwuBxLdbYh2UjhGi0Wx7B1v5Iw5GKhfFDQL7jM7SSp7K2g==
+ dependencies:
+ "@octokit/endpoint" "^5.5.0"
+ "@octokit/request-error" "^1.0.1"
+ "@octokit/types" "^2.0.0"
+ deprecation "^2.0.0"
+ is-plain-object "^3.0.0"
+ node-fetch "^2.3.0"
+ once "^1.4.0"
+ universal-user-agent "^5.0.0"
+
+"@octokit/types@^2.0.0":
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.3.1.tgz#40cd61c125a6161cfb3bfabc75805ac7a54213b4"
+ integrity sha512-rvJP1Y9A/+Cky2C3var1vsw3Lf5Rjn/0sojNl2AjCX+WbpIHYccaJ46abrZoIxMYnOToul6S9tPytUVkFI7CXQ==
+ dependencies:
+ "@types/node" ">= 8"
+
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
@@ -1548,6 +1596,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.8.tgz#92509422653f10e9c0ac18d87e0610b39f9821c7"
integrity sha512-8KmlRxwbKZfjUHFIt3q8TF5S2B+/E5BaAoo/3mgc5h6FJzqxXkCK/VMetO+IRDtwtU6HUvovHMBn+XRj7SV9Qg==
+"@types/node@>= 8":
+ version "13.7.7"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.7.tgz#1628e6461ba8cc9b53196dfeaeec7b07fa6eea99"
+ integrity sha512-Uo4chgKbnPNlxQwoFmYIwctkQVkMMmsAoGGU4JKwLuvBefF0pCq4FybNSnfkfRCpC7ZW7kttcC/TrRtAJsvGtg==
+
"@types/node@^10.1.0":
version "10.14.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.4.tgz#1c586b991457cbb58fef51bc4e0cfcfa347714b5"
@@ -1904,10 +1957,10 @@
"@webassemblyjs/wast-parser" "1.8.5"
"@xtuc/long" "4.2.2"
-"@webcomponents/custom-elements@^1.0.4":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.1.2.tgz#041e4c20df35245f4d160b50d044b8cff192962c"
- integrity sha512-gVhuQHLTrQ28v1qMp0WGPSCBukFL7qAlemxCf19TnuNZ0bO9KPF72bfhH6Hpuwdu9TptIMGNlqrr9PzqrzfZFQ==
+"@webcomponents/custom-elements@^1.1.0":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.4.0.tgz#f21f41804e1f708d1bc924c345413f8a0e9b51a3"
+ integrity sha512-t9xpI4KVH4IcVp9XqLIAxHOovNcaUDDcfjK3v7ORv7xjnbIL1Dc+735tUTgyJvugTPAaxunILJY+Rh6+5729Hw==
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
@@ -2346,6 +2399,11 @@ are-we-there-yet@~1.1.2:
delegates "^1.0.0"
readable-stream "^2.0.6"
+arg@^4.1.0:
+ version "4.1.3"
+ resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
+ integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
+
argparse@^1.0.7, argparse@~1.0.9:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@@ -4852,6 +4910,11 @@ deprecated@^0.0.1:
resolved "https://registry.yarnpkg.com/deprecated/-/deprecated-0.0.1.tgz#f9c9af5464afa1e7a971458a8bdef2aa94d5bb19"
integrity sha1-+cmvVGSvoeepcUWKi97yqpTVuxk=
+deprecation@^2.0.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
+ integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
+
des.js@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
@@ -8118,6 +8181,13 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
+is-plain-object@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.0.tgz#47bfc5da1b5d50d64110806c199359482e75a928"
+ integrity sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==
+ dependencies:
+ isobject "^4.0.0"
+
is-posix-bracket@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
@@ -8330,6 +8400,11 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+isobject@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
+ integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
+
isstream@0.1.x, isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -9472,6 +9547,11 @@ lru-queue@0.1:
dependencies:
es5-ext "~0.10.2"
+macos-release@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f"
+ integrity sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==
+
madge@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/madge/-/madge-3.6.0.tgz#f69e7c3e15a18a195e6bcd7942cc36efabcd9b9a"
@@ -9535,6 +9615,11 @@ make-dir@^3.0.0:
dependencies:
semver "^6.0.0"
+make-error@^1.1.1:
+ version "1.3.6"
+ resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
+ integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+
make-fetch-happen@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-5.0.0.tgz#a8e3fe41d3415dd656fe7b8e8172e1fb4458b38d"
@@ -10900,6 +10985,14 @@ os-locale@^3.0.0, os-locale@^3.1.0:
lcid "^2.0.0"
mem "^4.0.0"
+os-name@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801"
+ integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==
+ dependencies:
+ macos-release "^2.2.0"
+ windows-release "^3.1.0"
+
os-shim@^0.1.2:
version "0.1.3"
resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917"
@@ -13481,7 +13574,7 @@ source-map-support@0.4.3:
dependencies:
source-map "^0.5.3"
-source-map-support@0.5.16, source-map-support@^0.5.5, source-map-support@~0.5.12:
+source-map-support@0.5.16, source-map-support@^0.5.5, source-map-support@^0.5.6, source-map-support@~0.5.12:
version "0.5.16"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
@@ -14599,6 +14692,17 @@ try-require@^1.0.0:
resolved "https://registry.yarnpkg.com/try-require/-/try-require-1.2.1.tgz#34489a2cac0c09c1cc10ed91ba011594d4333be2"
integrity sha1-NEiaLKwMCcHMEO2RugEVlNQzO+I=
+ts-node@^8.6.2:
+ version "8.6.2"
+ resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.6.2.tgz#7419a01391a818fbafa6f826a33c1a13e9464e35"
+ integrity sha512-4mZEbofxGqLL2RImpe3zMJukvEvcO1XP8bj8ozBPySdCUXEcU5cIRwR0aM3R+VoZq7iXc8N86NC0FspGRqP4gg==
+ dependencies:
+ arg "^4.1.0"
+ diff "^4.0.1"
+ make-error "^1.1.1"
+ source-map-support "^0.5.6"
+ yn "3.1.1"
+
tsickle@0.38.0:
version "0.38.0"
resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.38.0.tgz#89f5952c9bb3ba0b36dc384975e23cf90e584822"
@@ -14750,6 +14854,11 @@ type@^2.0.0:
resolved "https://registry.yarnpkg.com/type/-/type-2.0.0.tgz#5f16ff6ef2eb44f260494dae271033b29c09a9c3"
integrity sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==
+typed-graphqlify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/typed-graphqlify/-/typed-graphqlify-2.3.0.tgz#c85f09b65b56e6da2c993899a34e551e2616bf91"
+ integrity sha512-iRjAneX/0gHBOrJnZGF5Nwl6OjHDiYBYxQzqenRJExVod0A0QHto9npX4d68i+2OuSHAzatNgSVtJoRgnBHeYQ==
+
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -14908,6 +15017,20 @@ universal-analytics@^0.4.16, universal-analytics@^0.4.20:
request "^2.88.0"
uuid "^3.0.0"
+universal-user-agent@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557"
+ integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg==
+ dependencies:
+ os-name "^3.1.0"
+
+universal-user-agent@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-5.0.0.tgz#a3182aa758069bf0e79952570ca757de3579c1d9"
+ integrity sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q==
+ dependencies:
+ os-name "^3.1.0"
+
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
@@ -15482,6 +15605,13 @@ widest-line@^2.0.0:
dependencies:
string-width "^2.1.1"
+windows-release@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f"
+ integrity sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==
+ dependencies:
+ execa "^1.0.0"
+
winreg@0.0.12:
version "0.0.12"
resolved "https://registry.yarnpkg.com/winreg/-/winreg-0.0.12.tgz#07105554ba1a9d08979251d129475bffae3006b7"
@@ -15766,6 +15896,11 @@ yeast@0.1.2:
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
+yn@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
+ integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
+
z-schema@~3.18.3:
version "3.18.4"
resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-3.18.4.tgz#ea8132b279533ee60be2485a02f7e3e42541a9a2"