/**
* @license
* Copyright Google LLC 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 {inspect} from 'util';
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {loadStandardTestFiles} from '../../src/ngtsc/testing';
import {tsSourceMapBug29300Fixed} from '../../src/ngtsc/util/src/ts_source_map_bug_29300';
import {NgtscTestEnvironment} from './env';
import {getMappedSegments, SegmentMapping} from './sourcemap_utils';
const testFiles = loadStandardTestFiles();
runInEachFileSystem((os) => {
describe('template source-mapping', () => {
let env!: NgtscTestEnvironment;
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
env.tsconfig({sourceMap: true, target: 'es2015', enableI18nLegacyMessageIdFormat: false});
});
describe('Inline templates', () => {
describe('(element creation)', () => {
it('should map simple element with content', () => {
const mappings = compileAndMap('
', generated: 'i0.ɵɵelementStart(0, "div")', sourceUrl: '../test.ts'});
expectMapping(mappings, {
source: '{{200.3 | percent : 2 }}',
generated: 'i0.ɵɵtextInterpolate(i0.ɵɵpipeBind2(2, 1, 200.3, 2))',
sourceUrl: '../test.ts'
});
expectMapping(
mappings,
{source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
});
describe('(property bindings)', () => {
it('should map a simple input binding expression', () => {
const mappings = compileAndMap('',
generated: 'i0.ɵɵelementStart(0, "div")',
sourceUrl: '../test.ts'
});
// TODO: Add better mappings for binding
expectMapping(
mappings,
{source: 'Message', generated: 'i0.ɵɵtext(1, "Message")', sourceUrl: '../test.ts'});
expectMapping(
mappings,
{source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
});
describe('(structural directives)', () => {
it('should map *ngIf scenario', () => {
const mappings = compileAndMap('',
generated: 'i0.ɵɵelementStart(0, "div")',
sourceUrl: '../test.ts'
});
// TODO - map the bindings better
expectMapping(
mappings,
{source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
// TODO: the `ctx_r...` appears to be dependent upon previous tests!!!
// expectMapping(mappings, {
// source: '{{ name }}',
// generated: 'i0.ɵɵtextInterpolate(ctx_r0.name)',
// sourceUrl: '../test.ts'
// });
});
it('should map ng-template [ngIf] scenario', () => {
const mappings = compileAndMap(
`', generated: 'i0.ɵɵelementStart(0, "div")', sourceUrl: '../test.ts'});
// TODO - map the bindings better
expectMapping(
mappings,
{source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
// TODO: the `ctx_r...` appears to be dependent upon previous tests!!!
// expectMapping(mappings, {
// source: '{{ name }}',
// generated: 'i0.ɵɵtextInterpolate(ctx_r0.name)',
// sourceUrl: '../test.ts'
// });
});
it('should map *ngFor scenario', () => {
const mappings = compileAndMap(
'',
generated: 'i0.ɵɵelementStart(0, "div")',
sourceUrl: '../test.ts'
});
// TODO - map the bindings better
expectMapping(
mappings,
{source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
it('should map ng-template [ngFor] scenario', () => {
const mappings = compileAndMap(
`', generated: 'i0.ɵɵelementStart(2, "div")', sourceUrl: '../test.ts'});
expectMapping(mappings, {
source: '',
generated: 'i0.ɵɵprojection(3, 1)',
sourceUrl: '../test.ts'
});
expectMapping(
mappings,
{source: '
', generated: 'i0.ɵɵelementEnd()', sourceUrl: '../test.ts'});
});
});
describe('$localize', () => {
it('should create simple i18n message source-mapping', () => {
const mappings = compileAndMap(`',
generated: 'i0.ɵɵelementStart(0, "div")',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: 'Hello, World!',
generated: '`Hello, World!`',
sourceUrl: '../test.ts',
});
});
it('should create placeholder source-mappings', () => {
const mappings = compileAndMap(`
Hello, {{name}}!
`);
expectMapping(mappings, {
source: '
',
generated: 'i0.ɵɵelementStart(0, "div")',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: '
',
generated: 'i0.ɵɵelementEnd()',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: 'Hello, ',
generated: '`Hello, ${',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: '{{name}}',
generated: '"\\uFFFD0\\uFFFD"',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: '!',
generated: '}:INTERPOLATION:!`',
sourceUrl: '../test.ts',
});
});
it('should correctly handle collapsed whitespace in interpolation placeholder source-mappings',
() => {
const mappings = compileAndMap(
`
pre-body {{greeting}} post-body
`);
expectMapping(mappings, {
source: '
',
generated: 'i0.ɵɵelementStart(0, "div", 0)',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: '
',
generated: 'i0.ɵɵelementEnd()',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: ' pre-body ',
generated: '` pre-body ${',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: '{{greeting}}',
generated: '"\\uFFFD0\\uFFFD"',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: ' post-body',
generated: '}:INTERPOLATION: post-body`',
sourceUrl: '../test.ts',
});
});
it('should correctly handle collapsed whitespace in element placeholder source-mappings',
() => {
const mappings =
compileAndMap(`
\n pre-p\n
\n in-p\n
\n post-p\n
`);
// $localize expressions
expectMapping(mappings, {
sourceUrl: '../test.ts',
source: 'pre-p\n ',
generated: '` pre-p ${',
});
expectMapping(mappings, {
sourceUrl: '../test.ts',
source: '
\n ',
generated: '"\\uFFFD#2\\uFFFD"',
});
expectMapping(mappings, {
sourceUrl: '../test.ts',
source: 'in-p\n ',
generated: '}:START_PARAGRAPH: in-p ${',
});
expectMapping(mappings, {
sourceUrl: '../test.ts',
source: '
\n ',
generated: '"\\uFFFD/#2\\uFFFD"',
});
expectMapping(mappings, {
sourceUrl: '../test.ts',
source: 'post-p\n',
generated: '}:CLOSE_PARAGRAPH: post-p\n`',
});
// ivy instructions
expectMapping(mappings, {
sourceUrl: '../test.ts',
source: '
',
generated: 'i0.ɵɵelementStart(0, "div")',
});
expectMapping(mappings, {
sourceUrl: '../test.ts',
source: '
',
generated: 'i0.ɵɵi18nStart(1, 0)',
});
expectMapping(mappings, {
sourceUrl: '../test.ts',
source: '
\n in-p\n
',
generated: 'i0.ɵɵelement(2, "p")',
});
expectMapping(mappings, {
sourceUrl: '../test.ts',
source: '
',
generated: 'i0.ɵɵi18nEnd()',
});
expectMapping(mappings, {
sourceUrl: '../test.ts',
source: '
',
generated: 'i0.ɵɵelementEnd()',
});
});
it('should create tag (container) placeholder source-mappings', () => {
const mappings = compileAndMap(`
Hello, World!
`);
expectMapping(mappings, {
source: '
',
generated: 'i0.ɵɵelementStart(0, "div")',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: '
',
generated: 'i0.ɵɵelementEnd()',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: 'Hello, ',
generated: '`Hello, ${',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: '
',
generated: '"\\uFFFD#2\\uFFFD"',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: 'World',
generated: '}:START_BOLD_TEXT:World${',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: '',
generated: '"\\uFFFD/#2\\uFFFD"',
sourceUrl: '../test.ts',
});
expectMapping(mappings, {
source: '!',
generated: '}:CLOSE_BOLD_TEXT:!`',
sourceUrl: '../test.ts',
});
});
});
it('should create (simple string) inline template source-mapping', () => {
const mappings = compileAndMap('
this is a test
{{ 1 + 2 }}
');
// Creation mode
expectMapping(
mappings,
{generated: 'i0.ɵɵelementStart(0, "div")', source: '
', sourceUrl: '../test.ts'});
expectMapping(mappings, {
generated: 'i0.ɵɵtext(1, "this is a test")',
source: 'this is a test',
sourceUrl: '../test.ts'
});
expectMapping(
mappings, {generated: 'i0.ɵɵelementEnd()', source: '
', sourceUrl: '../test.ts'});
expectMapping(
mappings,
{generated: 'i0.ɵɵelementStart(2, "div")', source: '
', sourceUrl: '../test.ts'});
expectMapping(
mappings, {generated: 'i0.ɵɵtext(3)', source: '{{ 1 + 2 }}', sourceUrl: '../test.ts'});
expectMapping(
mappings, {generated: 'i0.ɵɵelementEnd()', source: '
', sourceUrl: '../test.ts'});
// Update mode
expectMapping(mappings, {
generated: 'i0.ɵɵtextInterpolate(1 + 2)',
source: '{{ 1 + 2 }}',
sourceUrl: '../test.ts'
});
});
it('should create correct inline template source-mapping when the source contains escape sequences',
() => {
// Note that the escaped double quotes, which need un-escaping to be parsed correctly.
const mappings = compileAndMap('
this is a test
');
expectMapping(mappings, {
generated: 'i0.ɵɵelementStart(0, "div", 0)',
source: '
',
sourceUrl: '../test.ts'
});
const attrsMapping =
mappings.find(mapping => /consts: \[\[1, "some-class"\]\]/.test(mapping.generated));
expect(attrsMapping).toBeDefined();
});
});
if (tsSourceMapBug29300Fixed()) {
describe('External templates (where TS supports source-mapping)', () => {
it('should create external template source-mapping', () => {
const mappings =
compileAndMap('
this is a test
{{ 1 + 2 }}
', './dir/test.html');
// Creation mode
expectMapping(mappings, {
generated: 'i0.ɵɵelementStart(0, "div")',
source: '
',
sourceUrl: '../dir/test.html'
});
expectMapping(mappings, {
generated: 'i0.ɵɵtext(1, "this is a test")',
source: 'this is a test',
sourceUrl: '../dir/test.html'
});
expectMapping(
mappings,
{generated: 'i0.ɵɵelementEnd()', source: '
', sourceUrl: '../dir/test.html'});
expectMapping(mappings, {
generated: 'i0.ɵɵelementStart(2, "div")',
source: '
',
sourceUrl: '../dir/test.html'
});
expectMapping(
mappings,
{generated: 'i0.ɵɵtext(3)', source: '{{ 1 + 2 }}', sourceUrl: '../dir/test.html'});
expectMapping(
mappings,
{generated: 'i0.ɵɵelementEnd()', source: '
', sourceUrl: '../dir/test.html'});
// Update mode
expectMapping(mappings, {
generated: 'i0.ɵɵtextInterpolate(1 + 2)',
source: '{{ 1 + 2 }}',
sourceUrl: '../dir/test.html'
});
});
it('should create correct mappings when templateUrl is in a different rootDir', () => {
const mappings = compileAndMap(
'
this is a test
{{ 1 + 2 }}
', 'extraRootDir/test.html');
// Creation mode
expectMapping(mappings, {
generated: 'i0.ɵɵelementStart(0, "div")',
source: '
',
sourceUrl: '../extraRootDir/test.html'
});
expectMapping(mappings, {
generated: 'i0.ɵɵtext(1, "this is a test")',
source: 'this is a test',
sourceUrl: '../extraRootDir/test.html'
});
expectMapping(mappings, {
generated: 'i0.ɵɵelementEnd()',
source: '
',
sourceUrl: '../extraRootDir/test.html'
});
expectMapping(mappings, {
generated: 'i0.ɵɵelementStart(2, "div")',
source: '
',
sourceUrl: '../extraRootDir/test.html'
});
expectMapping(mappings, {
generated: 'i0.ɵɵtext(3)',
source: '{{ 1 + 2 }}',
sourceUrl: '../extraRootDir/test.html'
});
expectMapping(mappings, {
generated: 'i0.ɵɵelementEnd()',
source: '
',
sourceUrl: '../extraRootDir/test.html'
});
// Update mode
expectMapping(mappings, {
generated: 'i0.ɵɵtextInterpolate(1 + 2)',
source: '{{ 1 + 2 }}',
sourceUrl: '../extraRootDir/test.html'
});
});
});
}
function compileAndMap(template: string, templateUrl: string|null = null) {
const templateConfig = templateUrl ? `templateUrl: '${templateUrl}'` :
('template: `' + template.replace(/`/g, '\\`') + '`');
env.write('test.ts', `
import {Component, Directive, Input, Output, EventEmitter, Pipe, NgModule} from '@angular/core';
@Directive({
selector: '[ngModel],[attr],[ngModelChange]'
})
export class AllDirective {
@Input() ngModel!: any;
@Output() ngModelChange = new EventEmitter
();
@Input() attr!: any;
}
@Pipe({name: 'percent'})
export class PercentPipe {
transform(v: any) {}
}
@Component({
selector: 'test-cmp',
${templateConfig}
})
export class TestCmp {
name = '';
isInitial = false;
doSomething() {}
items: any[] = [];
greeting = '';
}
@NgModule({
declarations: [TestCmp, AllDirective, PercentPipe],
})
export class Module {}
`);
if (templateUrl) {
env.write(templateUrl, template);
}
env.driveMain();
return getMappedSegments(env, 'test.js');
}
/**
* Helper function for debugging failed mappings.
* This lays out the segment mappings in the console to make it easier to compare.
*/
function dumpMappings(mappings: SegmentMapping[]) {
mappings.forEach(map => {
// tslint:disable-next-line:no-console
console.log(
padValue(map.sourceUrl, 20, 0) + ' : ' + padValue(inspect(map.source), 100, 23) +
' : ' + inspect(map.generated));
});
function padValue(value: string, max: number, start: number) {
const padding = value.length > max ? ('\n' +
' '.repeat(max + start)) :
' '.repeat(max - value.length);
return value + padding;
}
}
function expectMapping(mappings: SegmentMapping[], expected: SegmentMapping): void {
if (mappings.some(
m => m.generated === expected.generated && m.source === expected.source &&
m.sourceUrl === expected.sourceUrl)) {
return;
}
const matchingGenerated = mappings.filter(m => m.generated === expected.generated);
const matchingSource = mappings.filter(m => m.source === expected.source);
const message = [
'Expected mappings to contain the following mapping',
prettyPrintMapping(expected),
];
if (matchingGenerated.length > 0) {
message.push('');
message.push('There are the following mappings that match the generated text:');
matchingGenerated.forEach(m => message.push(prettyPrintMapping(m)));
}
if (matchingSource.length > 0) {
message.push('');
message.push('There are the following mappings that match the source text:');
matchingSource.forEach(m => message.push(prettyPrintMapping(m)));
}
fail(message.join('\n'));
}
function prettyPrintMapping(mapping: SegmentMapping): string {
return [
'{',
` generated: ${JSON.stringify(mapping.generated)}`,
` source: ${JSON.stringify(mapping.source)}`,
` sourceUrl: ${JSON.stringify(mapping.sourceUrl)}`,
'}',
].join('\n');
}
});
});