From 492153a986285014e7a6835a24e0299d5c9b7359 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Wed, 15 Mar 2017 15:50:30 -0700 Subject: [PATCH] fix(compiler): make sourcemaps work in AOT mode Inlcuded fixes: - include preamble in generated source map - always add a mapping for line/col 0 so that the generated sourcemap is not sparse - use a uniue sourceUrl for inline templates even in the AOT case --- .../integrationtest/src/errors.html | 2 + .../integrationtest/src/errors.ts | 14 ++ .../integrationtest/src/module.ts | 3 + .../integrationtest/test/all_spec.ts | 3 + .../integrationtest/test/source_map_spec.ts | 41 +++++ .../integrationtest/tsconfig-build.json | 3 +- .../integrationtest/webpack.config.js | 6 +- packages/compiler-cli/src/codegen.ts | 3 +- packages/compiler/src/aot/compiler.ts | 6 +- packages/compiler/src/aot/compiler_factory.ts | 2 +- packages/compiler/src/aot/compiler_options.ts | 2 + packages/compiler/src/compile_metadata.ts | 4 +- .../compiler/src/output/abstract_emitter.ts | 27 +++- packages/compiler/src/output/js_emitter.ts | 22 +-- packages/compiler/src/output/output_jit.ts | 2 +- packages/compiler/src/output/ts_emitter.ts | 26 ++- packages/compiler/test/aot/compiler_spec.ts | 153 ++++++++++++------ .../output/abstract_emitter_node_only_spec.ts | 28 ++-- .../test/output/js_emitter_node_only_spec.ts | 17 +- .../compiler/test/output/js_emitter_spec.ts | 16 +- .../test/output/ts_emitter_node_only_spec.ts | 17 +- .../compiler/test/output/ts_emitter_spec.ts | 18 ++- .../source_map_integration_node_only_spec.ts | 6 +- scripts/ci/offline_compiler_test.sh | 1 + 24 files changed, 299 insertions(+), 123 deletions(-) create mode 100644 packages/compiler-cli/integrationtest/src/errors.html create mode 100644 packages/compiler-cli/integrationtest/src/errors.ts create mode 100644 packages/compiler-cli/integrationtest/test/source_map_spec.ts diff --git a/packages/compiler-cli/integrationtest/src/errors.html b/packages/compiler-cli/integrationtest/src/errors.html new file mode 100644 index 0000000000..6002658b13 --- /dev/null +++ b/packages/compiler-cli/integrationtest/src/errors.html @@ -0,0 +1,2 @@ +
+
\ No newline at end of file diff --git a/packages/compiler-cli/integrationtest/src/errors.ts b/packages/compiler-cli/integrationtest/src/errors.ts new file mode 100644 index 0000000000..fc25afe56e --- /dev/null +++ b/packages/compiler-cli/integrationtest/src/errors.ts @@ -0,0 +1,14 @@ +/** + * @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 {Component} from '@angular/core'; + +@Component({selector: 'comp-with-error', templateUrl: 'errors.html'}) +export class BindingErrorComp { + createError() { throw new Error('Test'); } +} diff --git a/packages/compiler-cli/integrationtest/src/module.ts b/packages/compiler-cli/integrationtest/src/module.ts index a6bfaa1e4d..1e1eacddf1 100644 --- a/packages/compiler-cli/integrationtest/src/module.ts +++ b/packages/compiler-cli/integrationtest/src/module.ts @@ -22,6 +22,7 @@ import {BasicComp} from './basic'; import {ComponentUsingThirdParty} from './comp_using_3rdp'; import {CUSTOM} from './custom_token'; import {CompWithAnalyzeEntryComponentsProvider, CompWithEntryComponents} from './entry_components'; +import {BindingErrorComp} from './errors'; import {CompConsumingEvents, CompUsingPipes, CompWithProviders, CompWithReferences, DirPublishingEvents, ModuleUsingCustomElements} from './features'; import {CompUsingRootModuleDirectiveAndPipe, SomeDirectiveInRootModule, SomeLibModule, SomePipeInRootModule, SomeService} from './module_fixtures'; import {CompWithNgContent, ProjectingComp} from './projection'; @@ -63,6 +64,7 @@ export const SERVER_ANIMATIONS_PROVIDERS: Provider[] = [{ SomeDirectiveInRootModule, SomePipeInRootModule, ComponentUsingThirdParty, + BindingErrorComp, ], imports: [ NoopAnimationsModule, @@ -88,6 +90,7 @@ export const SERVER_ANIMATIONS_PROVIDERS: Provider[] = [{ CompWithReferences, ProjectingComp, ComponentUsingThirdParty, + BindingErrorComp, ] }) export class MainModule { diff --git a/packages/compiler-cli/integrationtest/test/all_spec.ts b/packages/compiler-cli/integrationtest/test/all_spec.ts index cf9d214269..2bf078ec81 100644 --- a/packages/compiler-cli/integrationtest/test/all_spec.ts +++ b/packages/compiler-cli/integrationtest/test/all_spec.ts @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +require('source-map-support').install(); + import './init'; import './animate_spec'; import './basic_spec'; @@ -14,3 +16,4 @@ import './i18n_spec'; import './ng_module_spec'; import './projection_spec'; import './query_spec'; +import './source_map_spec'; \ No newline at end of file diff --git a/packages/compiler-cli/integrationtest/test/source_map_spec.ts b/packages/compiler-cli/integrationtest/test/source_map_spec.ts new file mode 100644 index 0000000000..8ce8e0d894 --- /dev/null +++ b/packages/compiler-cli/integrationtest/test/source_map_spec.ts @@ -0,0 +1,41 @@ +/** + * @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 './init'; +import {BindingErrorComp} from '../src/errors'; +import {createComponent} from './util'; + +describe('source maps', () => { + it('should report source location for binding errors', () => { + const comp = createComponent(BindingErrorComp); + let error: any; + try { + comp.detectChanges(); + } catch (e) { + error = e; + } + const sourcePosition = getSourcePositionForStack(error.stack); + expect(sourcePosition.line).toBe(2); + expect(sourcePosition.column).toBe(13); + expect(sourcePosition.source.endsWith('errors.html')).toBe(true); + }); +}); + +function getSourcePositionForStack(stack: string): {source: string, line: number, column: number} { + const htmlLocations = stack + .split('\n') + // e.g. at View_MyComp_0 (...html:153:40) + .map(line => /\((.*\.html):(\d+):(\d+)/.exec(line)) + .filter(match => !!match) + .map(match => ({ + source: match[1], + line: parseInt(match[2], 10), + column: parseInt(match[3], 10) + })); + return htmlLocations[0]; +} diff --git a/packages/compiler-cli/integrationtest/tsconfig-build.json b/packages/compiler-cli/integrationtest/tsconfig-build.json index 3c6446826f..bb4ec7c2de 100644 --- a/packages/compiler-cli/integrationtest/tsconfig-build.json +++ b/packages/compiler-cli/integrationtest/tsconfig-build.json @@ -17,7 +17,8 @@ "baseUrl": ".", // Prevent scanning up the directory tree for types "typeRoots": ["node_modules/@types"], - "noUnusedLocals": true + "noUnusedLocals": true, + "sourceMap": true }, "files": [ diff --git a/packages/compiler-cli/integrationtest/webpack.config.js b/packages/compiler-cli/integrationtest/webpack.config.js index cdee03eda2..504f93430c 100644 --- a/packages/compiler-cli/integrationtest/webpack.config.js +++ b/packages/compiler-cli/integrationtest/webpack.config.js @@ -11,5 +11,9 @@ module.exports = { entry: './test/all_spec.js', output: {filename: './all_spec.js'}, resolve: {extensions: ['.js']}, - + devtool: '#source-map', + module: { + loaders: + [{test: /\.js$/, exclude: /node_modules/, loaders: ['source-map-loader'], enforce: 'pre'}] + }, }; diff --git a/packages/compiler-cli/src/codegen.ts b/packages/compiler-cli/src/codegen.ts index ac74060d2f..7b37f98d2f 100644 --- a/packages/compiler-cli/src/codegen.ts +++ b/packages/compiler-cli/src/codegen.ts @@ -45,7 +45,7 @@ export class CodeGenerator { const sourceFile = this.program.getSourceFile(generatedModule.srcFileUrl); const emitPath = this.ngCompilerHost.calculateEmitPath(generatedModule.genFileUrl); const source = GENERATED_META_FILES.test(emitPath) ? generatedModule.source : - PREAMBLE + generatedModule.source; + generatedModule.source; this.host.writeFile(emitPath, source, false, () => {}, [sourceFile]); }); }); @@ -76,6 +76,7 @@ export class CodeGenerator { i18nFormat: cliOptions.i18nFormat, locale: cliOptions.locale, enableLegacyTemplate: options.enableLegacyTemplate !== false, + genFilePreamble: PREAMBLE, }); return new CodeGenerator(options, program, tsCompilerHost, aotCompiler, ngCompilerHost); } diff --git a/packages/compiler/src/aot/compiler.ts b/packages/compiler/src/aot/compiler.ts index 67ebc6d939..14bbefda8a 100644 --- a/packages/compiler/src/aot/compiler.ts +++ b/packages/compiler/src/aot/compiler.ts @@ -33,7 +33,8 @@ export class AotCompiler { private _styleCompiler: StyleCompiler, private _viewCompiler: ViewCompiler, private _ngModuleCompiler: NgModuleCompiler, private _outputEmitter: OutputEmitter, private _summaryResolver: SummaryResolver, private _localeId: string, - private _translationFormat: string, private _symbolResolver: StaticSymbolResolver) {} + private _translationFormat: string, private _genFilePreamble: string, + private _symbolResolver: StaticSymbolResolver) {} clearCache() { this._metadataResolver.clearCache(); } @@ -215,7 +216,8 @@ export class AotCompiler { exportedVars: string[]): GeneratedFile { return new GeneratedFile( srcFileUrl, genFileUrl, - this._outputEmitter.emitStatements(genFileUrl, statements, exportedVars)); + this._outputEmitter.emitStatements( + srcFileUrl, genFileUrl, statements, exportedVars, this._genFilePreamble)); } } diff --git a/packages/compiler/src/aot/compiler_factory.ts b/packages/compiler/src/aot/compiler_factory.ts index ce76cf414a..04c66ba638 100644 --- a/packages/compiler/src/aot/compiler_factory.ts +++ b/packages/compiler/src/aot/compiler_factory.ts @@ -79,6 +79,6 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom const compiler = new AotCompiler( config, compilerHost, resolver, tmplParser, new StyleCompiler(urlResolver), viewCompiler, new NgModuleCompiler(), new TypeScriptEmitter(importResolver), summaryResolver, - options.locale, options.i18nFormat, symbolResolver); + options.locale, options.i18nFormat, options.genFilePreamble, symbolResolver); return {compiler, reflector: staticReflector}; } diff --git a/packages/compiler/src/aot/compiler_options.ts b/packages/compiler/src/aot/compiler_options.ts index 96b0c9b661..d5c369dc3c 100644 --- a/packages/compiler/src/aot/compiler_options.ts +++ b/packages/compiler/src/aot/compiler_options.ts @@ -11,4 +11,6 @@ export interface AotCompilerOptions { i18nFormat?: string; translations?: string; enableLegacyTemplate?: boolean; + /** preamble for all generated source files */ + genFilePreamble?: string; } diff --git a/packages/compiler/src/compile_metadata.ts b/packages/compiler/src/compile_metadata.ts index 289b619e87..5d251c69d2 100644 --- a/packages/compiler/src/compile_metadata.ts +++ b/packages/compiler/src/compile_metadata.ts @@ -772,7 +772,9 @@ export function templateSourceUrl( templateMeta: {isInline: boolean, templateUrl: string}) { if (templateMeta.isInline) { if (compMeta.type.reference instanceof StaticSymbol) { - return compMeta.type.reference.filePath; + // Note: a .ts file might contain multiple components with inline templates, + // so we need to give them unique urls, as these will be used for sourcemaps. + return `${compMeta.type.reference.filePath}#${compMeta.type.reference.name}.html`; } else { return `${ngJitFolder()}/${identifierName(ngModuleType)}/${identifierName(compMeta.type)}.html`; } diff --git a/packages/compiler/src/output/abstract_emitter.ts b/packages/compiler/src/output/abstract_emitter.ts index a201cd1227..f7082d6f6b 100644 --- a/packages/compiler/src/output/abstract_emitter.ts +++ b/packages/compiler/src/output/abstract_emitter.ts @@ -18,7 +18,9 @@ export const CATCH_ERROR_VAR = o.variable('error'); export const CATCH_STACK_VAR = o.variable('stack'); export abstract class OutputEmitter { - abstract emitStatements(moduleUrl: string, stmts: o.Statement[], exportedVars: string[]): string; + abstract emitStatements( + srcFilePath: string, genFilePath: string, stmts: o.Statement[], exportedVars: string[], + preamble?: string): string; } class _EmittedLine { @@ -89,13 +91,24 @@ export class EmitterVisitorContext { .join('\n'); } - toSourceMapGenerator(file: string|null = null, startsAtLine: number = 0): SourceMapGenerator { - const map = new SourceMapGenerator(file); + toSourceMapGenerator(sourceFilePath: string, genFilePath: string, startsAtLine: number = 0): + SourceMapGenerator { + const map = new SourceMapGenerator(genFilePath); + + let firstOffsetMapped = false; + const mapFirstOffsetIfNeeded = () => { + if (!firstOffsetMapped) { + map.addSource(sourceFilePath).addMapping(0, sourceFilePath, 0, 0); + firstOffsetMapped = true; + } + }; + for (let i = 0; i < startsAtLine; i++) { map.addLine(); + mapFirstOffsetIfNeeded(); } - this.sourceLines.forEach(line => { + this.sourceLines.forEach((line, lineIdx) => { map.addLine(); const spans = line.srcSpans; @@ -107,13 +120,17 @@ export class EmitterVisitorContext { col0 += parts[spanIdx].length; spanIdx++; } + if (spanIdx < spans.length && lineIdx === 0 && col0 === 0) { + firstOffsetMapped = true; + } else { + mapFirstOffsetIfNeeded(); + } while (spanIdx < spans.length) { const span = spans[spanIdx]; const source = span.start.file; const sourceLine = span.start.line; const sourceCol = span.start.col; - map.addSource(source.url, source.content) .addMapping(col0, source.url, sourceLine, sourceCol); diff --git a/packages/compiler/src/output/js_emitter.ts b/packages/compiler/src/output/js_emitter.ts index dac480288e..5f478a83a8 100644 --- a/packages/compiler/src/output/js_emitter.ts +++ b/packages/compiler/src/output/js_emitter.ts @@ -18,29 +18,29 @@ import {ImportResolver} from './path_util'; export class JavaScriptEmitter implements OutputEmitter { constructor(private _importResolver: ImportResolver) {} - emitStatements(genFilePath: string, stmts: o.Statement[], exportedVars: string[]): string { + emitStatements( + srcFilePath: string, genFilePath: string, stmts: o.Statement[], exportedVars: string[], + preamble: string = ''): string { const converter = new JsEmitterVisitor(genFilePath, this._importResolver); const ctx = EmitterVisitorContext.createRoot(exportedVars); converter.visitAllStatements(stmts, ctx); - const srcParts: string[] = []; + const preambleLines = preamble ? preamble.split('\n') : []; converter.importsWithPrefixes.forEach((prefix, importedFilePath) => { // Note: can't write the real word for import as it screws up system.js auto detection... - srcParts.push( + preambleLines.push( `var ${prefix} = req` + `uire('${this._importResolver.fileNameToModuleName(importedFilePath, genFilePath)}');`); }); - srcParts.push(ctx.toSource()); - - const prefixLines = converter.importsWithPrefixes.size; - const sm = ctx.toSourceMapGenerator(genFilePath, prefixLines).toJsComment(); + const sm = + ctx.toSourceMapGenerator(srcFilePath, genFilePath, preambleLines.length).toJsComment(); + const lines = [...preambleLines, ctx.toSource(), sm]; if (sm) { - srcParts.push(sm); + // always add a newline at the end, as some tools have bugs without it. + lines.push(''); } - // always add a newline at the end, as some tools have bugs without it. - srcParts.push(''); - return srcParts.join('\n'); + return lines.join('\n'); } } diff --git a/packages/compiler/src/output/output_jit.ts b/packages/compiler/src/output/output_jit.ts index 7ba5d1cd2f..660c5ffc7f 100644 --- a/packages/compiler/src/output/output_jit.ts +++ b/packages/compiler/src/output/output_jit.ts @@ -30,7 +30,7 @@ function evalExpression( // We don't want to hard code this fact, so we auto detect it via an empty function first. const emptyFn = new Function(...fnArgNames.concat('return null;')).toString(); const headerLines = emptyFn.slice(0, emptyFn.indexOf('return null;')).split('\n').length - 1; - fnBody += `\n${ctx.toSourceMapGenerator(sourceUrl, headerLines).toJsComment()}`; + fnBody += `\n${ctx.toSourceMapGenerator(sourceUrl, sourceUrl, headerLines).toJsComment()}`; } return new Function(...fnArgNames.concat(fnBody))(...fnArgValues); } diff --git a/packages/compiler/src/output/ts_emitter.ts b/packages/compiler/src/output/ts_emitter.ts index bd22cebdbd..d6ceecb2a4 100644 --- a/packages/compiler/src/output/ts_emitter.ts +++ b/packages/compiler/src/output/ts_emitter.ts @@ -44,40 +44,38 @@ export function debugOutputAstAsTypeScript(ast: o.Statement | o.Expression | o.T export class TypeScriptEmitter implements OutputEmitter { constructor(private _importResolver: ImportResolver) {} - emitStatements(genFilePath: string, stmts: o.Statement[], exportedVars: string[]): string { + emitStatements( + srcFilePath: string, genFilePath: string, stmts: o.Statement[], exportedVars: string[], + preamble: string = ''): string { const converter = new _TsEmitterVisitor(genFilePath, this._importResolver); const ctx = EmitterVisitorContext.createRoot(exportedVars); converter.visitAllStatements(stmts, ctx); - const srcParts: string[] = []; - + const preambleLines = preamble ? preamble.split('\n') : []; converter.reexports.forEach((reexports, exportedFilePath) => { const reexportsCode = reexports.map(reexport => `${reexport.name} as ${reexport.as}`).join(','); - srcParts.push( + preambleLines.push( `export {${reexportsCode}} from '${this._importResolver.fileNameToModuleName(exportedFilePath, genFilePath)}';`); }); converter.importsWithPrefixes.forEach((prefix, importedFilePath) => { // Note: can't write the real word for import as it screws up system.js auto detection... - srcParts.push( + preambleLines.push( `imp` + `ort * as ${prefix} from '${this._importResolver.fileNameToModuleName(importedFilePath, genFilePath)}';`); }); - srcParts.push(ctx.toSource()); - - const prefixLines = converter.reexports.size + converter.importsWithPrefixes.size; - const sm = ctx.toSourceMapGenerator(genFilePath, prefixLines).toJsComment(); + const sm = + ctx.toSourceMapGenerator(srcFilePath, genFilePath, preambleLines.length).toJsComment(); + const lines = [...preambleLines, ctx.toSource(), sm]; if (sm) { - srcParts.push(sm); + // always add a newline at the end, as some tools have bugs without it. + lines.push(''); } - // always add a newline at the end, as some tools have bugs without it. - srcParts.push(''); - - return srcParts.join('\n'); + return lines.join('\n'); } } diff --git a/packages/compiler/test/aot/compiler_spec.ts b/packages/compiler/test/aot/compiler_spec.ts index 846fb5a7f9..34192e8d06 100644 --- a/packages/compiler/test/aot/compiler_spec.ts +++ b/packages/compiler/test/aot/compiler_spec.ts @@ -8,7 +8,7 @@ import {AotCompiler, AotCompilerHost, AotCompilerOptions, GeneratedFile, createAotCompiler} from '@angular/compiler'; import {RenderComponentType, ɵReflectionCapabilities as ReflectionCapabilities, ɵreflector as reflector} from '@angular/core'; -import {async, fakeAsync, tick} from '@angular/core/testing'; +import {async} from '@angular/core/testing'; import {MetadataBundler, MetadataCollector, ModuleMetadata, privateEntriesToIndex} from '@angular/tsc-wrapped'; import * as ts from 'typescript'; @@ -94,19 +94,18 @@ describe('compiler (unbundled Angular)', () => { rootDir = {'app': appDir}; }); - function compileApp(): GeneratedFile { - const host = new MockCompilerHost(['/app/app.module.ts'], rootDir, angularFiles); - const aotHost = new MockAotCompilerHost(host); - let result: GeneratedFile[]; - let error: Error; - compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics) - .then((files) => result = files, (err) => error = err); - tick(); - if (error) { - throw error; - } - return result.find(genFile => genFile.srcFileUrl === componentPath); - ; + function compileApp(): Promise { + return new Promise((resolve, reject) => { + const host = new MockCompilerHost(['/app/app.module.ts'], rootDir, angularFiles); + const aotHost = new MockAotCompilerHost(host); + let result: GeneratedFile[]; + let error: Error; + resolve(compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics) + .then( + (files) => files.find( + genFile => genFile.srcFileUrl === componentPath && + genFile.genFileUrl.endsWith('.ts')))); + }); } function findLineAndColumn(file: string, token: string): {line: number, column: number} { @@ -134,7 +133,7 @@ describe('compiler (unbundled Angular)', () => { } describe('inline templates', () => { - const templateUrl = componentPath; + const templateUrl = `${componentPath}#AppComponent.html`; function templateDecorator(template: string) { return `template: \`${template}\`,`; } @@ -155,74 +154,96 @@ describe('compiler (unbundled Angular)', () => { function declareTests( {templateUrl, templateDecorator}: {templateUrl: string, templateDecorator: (template: string) => string}) { - it('should use the right source url in html parse errors', fakeAsync(() => { + it('should use the right source url in html parse errors', async(() => { appDir['app.component.ts'] = createComponentSource(templateDecorator('
\n ')); - expect(() => compileApp()) - .toThrowError(new RegExp(`Template parse errors[\\s\\S]*${templateUrl}@1:2`)); + expectPromiseToThrow( + compileApp(), new RegExp(`Template parse errors[\\s\\S]*${templateUrl}@1:2`)); })); - it('should use the right source url in template parse errors', fakeAsync(() => { + it('should use the right source url in template parse errors', async(() => { appDir['app.component.ts'] = createComponentSource( templateDecorator('
\n
')); - expect(() => compileApp()) - .toThrowError(new RegExp(`Template parse errors[\\s\\S]*${templateUrl}@1:7`)); + expectPromiseToThrow( + compileApp(), new RegExp(`Template parse errors[\\s\\S]*${templateUrl}@1:7`)); })); - it('should create a sourceMap for the template', fakeAsync(() => { + it('should create a sourceMap for the template', async(() => { const template = 'Hello World!'; appDir['app.component.ts'] = createComponentSource(templateDecorator(template)); - const genFile = compileApp(); - const sourceMap = extractSourceMap(genFile.source); - expect(sourceMap.file).toEqual(genFile.genFileUrl); - // the generated file contains the host view and the component view. - // we are only interested in the component view. - const sourceIndex = sourceMap.sources.indexOf(templateUrl); - expect(sourceMap.sourcesContent[sourceIndex]).toEqual(template); + compileApp().then((genFile) => { + const sourceMap = extractSourceMap(genFile.source); + expect(sourceMap.file).toEqual(genFile.genFileUrl); + + // the generated file contains code that is not mapped to + // the template but rather to the original source file (e.g. import statements, ...) + const templateIndex = sourceMap.sources.indexOf(templateUrl); + expect(sourceMap.sourcesContent[templateIndex]).toEqual(template); + + // for the mapping to the original source file we don't store the source code + // as we want to keep whatever TypeScript / ... produced for them. + const sourceIndex = sourceMap.sources.indexOf(componentPath); + expect(sourceMap.sourcesContent[sourceIndex]).toBe(null); + }); })); - it('should map elements correctly to the source', fakeAsync(() => { + it('should map elements correctly to the source', async(() => { const template = '
\n
'; appDir['app.component.ts'] = createComponentSource(templateDecorator(template)); - const genFile = compileApp(); - const sourceMap = extractSourceMap(genFile.source); - expect(originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `'span'`))) - .toEqual({line: 2, column: 3, source: templateUrl}); + compileApp().then((genFile) => { + const sourceMap = extractSourceMap(genFile.source); + expect(originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `'span'`))) + .toEqual({line: 2, column: 3, source: templateUrl}); + }); })); - it('should map bindings correctly to the source', fakeAsync(() => { + it('should map bindings correctly to the source', async(() => { const template = `
\n
`; appDir['app.component.ts'] = createComponentSource(templateDecorator(template)); - const genFile = compileApp(); - const sourceMap = extractSourceMap(genFile.source); - expect(originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `someMethod()`))) - .toEqual({line: 2, column: 9, source: templateUrl}); + compileApp().then((genFile) => { + const sourceMap = extractSourceMap(genFile.source); + expect( + originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `someMethod()`))) + .toEqual({line: 2, column: 9, source: templateUrl}); + }); })); - it('should map events correctly to the source', fakeAsync(() => { + it('should map events correctly to the source', async(() => { const template = `
\n
`; appDir['app.component.ts'] = createComponentSource(templateDecorator(template)); - const genFile = compileApp(); - const sourceMap = extractSourceMap(genFile.source); - expect(originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `someMethod()`))) - .toEqual({line: 2, column: 9, source: templateUrl}); + compileApp().then((genFile) => { + const sourceMap = extractSourceMap(genFile.source); + expect( + originalPositionFor(sourceMap, findLineAndColumn(genFile.source, `someMethod()`))) + .toEqual({line: 2, column: 9, source: templateUrl}); + }); + })); + + it('should map non template parts to the source file', async(() => { + appDir['app.component.ts'] = createComponentSource(templateDecorator('Hello World!')); + + compileApp().then((genFile) => { + const sourceMap = extractSourceMap(genFile.source); + expect(originalPositionFor(sourceMap, {line: 1, column: 0})) + .toEqual({line: 1, column: 0, source: componentPath}); + }); })); } }); describe('errors', () => { it('should only warn if not all arguments of an @Injectable class can be resolved', - fakeAsync(() => { + async(() => { const FILES: MockData = { app: { 'app.ts': ` @@ -237,17 +258,40 @@ describe('compiler (unbundled Angular)', () => { }; const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles); const aotHost = new MockAotCompilerHost(host); - let ok = false; const warnSpy = spyOn(console, 'warn'); - compile(host, aotHost, expectNoDiagnostics).then(() => ok = true); + compile(host, aotHost, expectNoDiagnostics).then(() => { + expect(warnSpy).toHaveBeenCalledWith( + `Warning: Can't resolve all parameters for MyService in /app/app.ts: (?). This will become an error in Angular v5.x`); + }); - tick(); - - expect(ok).toBe(true); - expect(warnSpy).toHaveBeenCalledWith( - `Warning: Can't resolve all parameters for MyService in /app/app.ts: (?). This will become an error in Angular v5.x`); })); }); + + it('should add the preamble to generated files', async(() => { + const FILES: MockData = { + app: { + 'app.ts': ` + import { NgModule, Component } from '@angular/core'; + + @Component({ template: '' }) + export class AppComponent {} + + @NgModule({ declarations: [ AppComponent ] }) + export class AppModule { } + ` + } + }; + const host = new MockCompilerHost(['/app/app.ts'], FILES, angularFiles); + const aotHost = new MockAotCompilerHost(host); + const genFilePreamble = '/* Hello world! */'; + compile(host, aotHost, expectNoDiagnostics, expectNoDiagnostics, {genFilePreamble}) + .then((generatedFiles) => { + const genFile = generatedFiles.find( + gf => gf.srcFileUrl === '/app/app.ts' && gf.genFileUrl.endsWith('.ts')); + expect(genFile.source.startsWith(genFilePreamble)).toBe(true); + }); + + })); }); describe('compiler (bundled Angular)', () => { @@ -426,3 +470,8 @@ const FILES: MockData = { } } }; + +function expectPromiseToThrow(p: Promise, msg: RegExp) { + p.then( + () => { throw new Error('Expected to throw'); }, (e) => { expect(e.message).toMatch(msg); }); +} \ No newline at end of file diff --git a/packages/compiler/test/output/abstract_emitter_node_only_spec.ts b/packages/compiler/test/output/abstract_emitter_node_only_spec.ts index 103a593614..3a76348f49 100644 --- a/packages/compiler/test/output/abstract_emitter_node_only_spec.ts +++ b/packages/compiler/test/output/abstract_emitter_node_only_spec.ts @@ -25,7 +25,7 @@ export function main() { ctx.print(createSourceSpan(fileA, 1), 'o1'); ctx.print(createSourceSpan(fileB, 0), 'o2'); ctx.print(createSourceSpan(fileB, 1), 'o3'); - const sm = ctx.toSourceMapGenerator('o.js').toJSON(); + const sm = ctx.toSourceMapGenerator('o.ts', 'o.js').toJSON(); expect(sm.sources).toEqual([fileA.url, fileB.url]); expect(sm.sourcesContent).toEqual([fileA.content, fileB.content]); }); @@ -43,7 +43,7 @@ export function main() { it('should be able to shift the content', () => { ctx.print(createSourceSpan(fileA, 0), 'fileA-0'); - const sm = ctx.toSourceMapGenerator(null, 10).toJSON(); + const sm = ctx.toSourceMapGenerator('o.ts', 'o.js', 10).toJSON(); expect(originalPositionFor(sm, {line: 11, column: 0})).toEqual({ line: 1, column: 0, @@ -51,13 +51,23 @@ export function main() { }); }); - it('should not map leading segment without span', () => { + it('should use the default source file for the first character', () => { + ctx.print(null, 'fileA-0'); + expectMap(ctx, 0, 0, 'o.ts', 0, 0); + }); + + it('should use an explicit mapping for the first character', () => { + ctx.print(createSourceSpan(fileA, 0), 'fileA-0'); + expectMap(ctx, 0, 0, 'a.js', 0, 0); + }); + + it('should map leading segment without span', () => { ctx.print(null, '....'); ctx.print(createSourceSpan(fileA, 0), 'fileA-0'); - expectMap(ctx, 0, 0); + expectMap(ctx, 0, 0, 'o.ts', 0, 0); expectMap(ctx, 0, 4, 'a.js', 0, 0); - expect(nbSegmentsPerLine(ctx)).toEqual([1]); + expect(nbSegmentsPerLine(ctx)).toEqual([2]); }); it('should handle indent', () => { @@ -68,7 +78,7 @@ export function main() { ctx.decIndent(); ctx.println(createSourceSpan(fileA, 2), 'fileA-2'); - expectMap(ctx, 0, 0); + expectMap(ctx, 0, 0, 'o.ts', 0, 0); expectMap(ctx, 0, 2, 'a.js', 0, 0); expectMap(ctx, 1, 0); expectMap(ctx, 1, 2); @@ -76,7 +86,7 @@ export function main() { expectMap(ctx, 2, 0); expectMap(ctx, 2, 2, 'a.js', 0, 4); - expect(nbSegmentsPerLine(ctx)).toEqual([1, 1, 1]); + expect(nbSegmentsPerLine(ctx)).toEqual([2, 1, 1]); }); it('should coalesce identical span', () => { @@ -103,7 +113,7 @@ export function main() { function expectMap( ctx: EmitterVisitorContext, genLine: number, genCol: number, source: string = null, srcLine: number = null, srcCol: number = null) { - const sm = ctx.toSourceMapGenerator().toJSON(); + const sm = ctx.toSourceMapGenerator('o.ts', 'o.js').toJSON(); const genPosition = {line: genLine + 1, column: genCol}; const origPosition = originalPositionFor(sm, genPosition); expect(origPosition.source).toEqual(source); @@ -113,7 +123,7 @@ function expectMap( // returns the number of segments per line function nbSegmentsPerLine(ctx: EmitterVisitorContext) { - const sm = ctx.toSourceMapGenerator().toJSON(); + const sm = ctx.toSourceMapGenerator('o.ts', 'o.js').toJSON(); const lines = sm.mappings.split(';'); return lines.map(l => { const m = l.match(/,/g); diff --git a/packages/compiler/test/output/js_emitter_node_only_spec.ts b/packages/compiler/test/output/js_emitter_node_only_spec.ts index 61c529e92a..c5bcf79ac2 100644 --- a/packages/compiler/test/output/js_emitter_node_only_spec.ts +++ b/packages/compiler/test/output/js_emitter_node_only_spec.ts @@ -16,7 +16,8 @@ import {ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler import {extractSourceMap, originalPositionFor} from './source_map_util'; -const someModuleUrl = 'somePackage/somePath'; +const someGenFilePath = 'somePackage/someGenFile'; +const someSourceFilePath = 'somePackage/someSourceFile'; class SimpleJsImportGenerator implements ImportResolver { fileNameToModuleName(importedUrlStr: string, moduleUrlStr: string): string { @@ -38,9 +39,11 @@ export function main() { }); function emitSourceMap( - stmt: o.Statement | o.Statement[], exportedVars: string[] = null): SourceMap { + stmt: o.Statement | o.Statement[], exportedVars: string[] = null, + preamble?: string): SourceMap { const stmts = Array.isArray(stmt) ? stmt : [stmt]; - const source = emitter.emitStatements(someModuleUrl, stmts, exportedVars || []); + const source = emitter.emitStatements( + someSourceFilePath, someGenFilePath, stmts, exportedVars || [], preamble); return extractSourceMap(source); } @@ -51,11 +54,11 @@ export function main() { const endLocation = new ParseLocation(source, 7, 0, 6); const sourceSpan = new ParseSourceSpan(startLocation, endLocation); const someVar = o.variable('someVar', null, sourceSpan); - const sm = emitSourceMap(someVar.toStmt()); + const sm = emitSourceMap(someVar.toStmt(), [], '/* MyPreamble \n */'); - expect(sm.sources).toEqual(['in.js']); - expect(sm.sourcesContent).toEqual([';;;var']); - expect(originalPositionFor(sm, {line: 1, column: 0})) + expect(sm.sources).toEqual([someSourceFilePath, 'in.js']); + expect(sm.sourcesContent).toEqual([null, ';;;var']); + expect(originalPositionFor(sm, {line: 3, column: 0})) .toEqual({line: 1, column: 3, source: 'in.js'}); }); }); diff --git a/packages/compiler/test/output/js_emitter_spec.ts b/packages/compiler/test/output/js_emitter_spec.ts index 6af6f22491..582bcb881d 100644 --- a/packages/compiler/test/output/js_emitter_spec.ts +++ b/packages/compiler/test/output/js_emitter_spec.ts @@ -14,11 +14,12 @@ import {ImportResolver} from '@angular/compiler/src/output/path_util'; import {stripSourceMapAndNewLine} from './abstract_emitter_spec'; -const someModuleUrl = 'somePackage/somePath'; +const someGenFilePath = 'somePackage/someGenFile'; +const someSourceFilePath = 'somePackage/someSourceFile'; const anotherModuleUrl = 'somePackage/someOtherPath'; const sameModuleIdentifier: CompileIdentifierMetadata = { - reference: new StaticSymbol(someModuleUrl, 'someLocalId', []) + reference: new StaticSymbol(someGenFilePath, 'someLocalId', []) }; const externalModuleIdentifier: CompileIdentifierMetadata = { reference: new StaticSymbol(anotherModuleUrl, 'someExternalId', []) @@ -48,8 +49,9 @@ export function main() { someVar = o.variable('someVar'); }); - function emitStmt(stmt: o.Statement, exportedVars: string[] = null): string { - const source = emitter.emitStatements(someModuleUrl, [stmt], exportedVars || []); + function emitStmt(stmt: o.Statement, exportedVars: string[] = null, preamble?: string): string { + const source = emitter.emitStatements( + someSourceFilePath, someGenFilePath, [stmt], exportedVars || [], preamble); return stripSourceMapAndNewLine(source); } @@ -300,5 +302,11 @@ export function main() { ].join('\n')); }); }); + + it('should support a preamble', () => { + expect(emitStmt(o.variable('a').toStmt(), [], '/* SomePreamble */')).toBe([ + '/* SomePreamble */', 'a;' + ].join('\n')); + }); }); } diff --git a/packages/compiler/test/output/ts_emitter_node_only_spec.ts b/packages/compiler/test/output/ts_emitter_node_only_spec.ts index 050bfe1c49..2c1a2869b8 100644 --- a/packages/compiler/test/output/ts_emitter_node_only_spec.ts +++ b/packages/compiler/test/output/ts_emitter_node_only_spec.ts @@ -16,7 +16,8 @@ import {ParseSourceSpan} from '@angular/compiler/src/parse_util'; import {extractSourceMap, originalPositionFor} from './source_map_util'; -const someModuleUrl = 'somePackage/somePath'; +const someGenFilePath = 'somePackage/someGenFile'; +const someSourceFilePath = 'somePackage/someSourceFile'; class SimpleJsImportGenerator implements ImportResolver { fileNameToModuleName(importedUrlStr: string, moduleUrlStr: string): string { @@ -43,9 +44,11 @@ export function main() { }); function emitSourceMap( - stmt: o.Statement | o.Statement[], exportedVars: string[] = null): SourceMap { + stmt: o.Statement | o.Statement[], exportedVars: string[] = null, + preamble?: string): SourceMap { const stmts = Array.isArray(stmt) ? stmt : [stmt]; - const source = emitter.emitStatements(someModuleUrl, stmts, exportedVars || []); + const source = emitter.emitStatements( + someSourceFilePath, someGenFilePath, stmts, exportedVars || [], preamble); return extractSourceMap(source); } @@ -56,11 +59,11 @@ export function main() { const endLocation = new ParseLocation(source, 7, 0, 6); const sourceSpan = new ParseSourceSpan(startLocation, endLocation); const someVar = o.variable('someVar', null, sourceSpan); - const sm = emitSourceMap(someVar.toStmt()); + const sm = emitSourceMap(someVar.toStmt(), [], '/* MyPreamble \n */'); - expect(sm.sources).toEqual(['in.js']); - expect(sm.sourcesContent).toEqual([';;;var']); - expect(originalPositionFor(sm, {line: 1, column: 0})) + expect(sm.sources).toEqual([someSourceFilePath, 'in.js']); + expect(sm.sourcesContent).toEqual([null, ';;;var']); + expect(originalPositionFor(sm, {line: 3, column: 0})) .toEqual({line: 1, column: 3, source: 'in.js'}); }); }); diff --git a/packages/compiler/test/output/ts_emitter_spec.ts b/packages/compiler/test/output/ts_emitter_spec.ts index 70b5b33887..6d4bb0f1ba 100644 --- a/packages/compiler/test/output/ts_emitter_spec.ts +++ b/packages/compiler/test/output/ts_emitter_spec.ts @@ -14,11 +14,12 @@ import {TypeScriptEmitter} from '@angular/compiler/src/output/ts_emitter'; import {stripSourceMapAndNewLine} from './abstract_emitter_spec'; -const someModuleUrl = 'somePackage/somePath'; +const someGenFilePath = 'somePackage/someGenFile'; +const someSourceFilePath = 'somePackage/someSourceFile'; const anotherModuleUrl = 'somePackage/someOtherPath'; const sameModuleIdentifier: CompileIdentifierMetadata = { - reference: new StaticSymbol(someModuleUrl, 'someLocalId', []) + reference: new StaticSymbol(someGenFilePath, 'someLocalId', []) }; const externalModuleIdentifier: CompileIdentifierMetadata = { @@ -49,9 +50,12 @@ export function main() { someVar = o.variable('someVar'); }); - function emitStmt(stmt: o.Statement | o.Statement[], exportedVars: string[] = null): string { + function emitStmt( + stmt: o.Statement | o.Statement[], exportedVars: string[] = null, + preamble?: string): string { const stmts = Array.isArray(stmt) ? stmt : [stmt]; - const source = emitter.emitStatements(someModuleUrl, stmts, exportedVars || []); + const source = emitter.emitStatements( + someSourceFilePath, someGenFilePath, stmts, exportedVars || [], preamble); return stripSourceMapAndNewLine(source); } @@ -459,5 +463,11 @@ export function main() { expect(emitStmt(writeVarExpr.toDeclStmt(new o.MapType(o.INT_TYPE)))) .toEqual('var a:{[key: string]:number} = (null as any);'); }); + + it('should support a preamble', () => { + expect(emitStmt(o.variable('a').toStmt(), [], '/* SomePreamble */')).toBe([ + '/* SomePreamble */', 'a;' + ].join('\n')); + }); }); } diff --git a/packages/core/test/linker/source_map_integration_node_only_spec.ts b/packages/core/test/linker/source_map_integration_node_only_spec.ts index bdcb4d4b6e..ad53d8fa10 100644 --- a/packages/core/test/linker/source_map_integration_node_only_spec.ts +++ b/packages/core/test/linker/source_map_integration_node_only_spec.ts @@ -125,8 +125,10 @@ export function main() { compileAndCreateComponent(MyComp); const sourceMap = getSourceMap('ng:///DynamicTestModule/MyComp.ngfactory.js'); - expect(sourceMap.sources).toEqual([templateUrl]); - expect(sourceMap.sourcesContent).toEqual([template]); + expect(sourceMap.sources).toEqual([ + 'ng:///DynamicTestModule/MyComp.ngfactory.js', templateUrl + ]); + expect(sourceMap.sourcesContent).toEqual([null, template]); })); diff --git a/scripts/ci/offline_compiler_test.sh b/scripts/ci/offline_compiler_test.sh index b0cef0f2d9..db0a29daeb 100755 --- a/scripts/ci/offline_compiler_test.sh +++ b/scripts/ci/offline_compiler_test.sh @@ -17,6 +17,7 @@ PKGS=( @types/{node@6.0.38,jasmine@2.2.33} jasmine@2.4.1 webpack@2.1.0-beta.21 + source-map-loader@0.2.0 @angular2-material/{core,button}@2.0.0-alpha.8-1 )