diff --git a/gulpfile.js b/gulpfile.js index a4c6ead9d1..2a907f23ab 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -36,6 +36,7 @@ gulp.task('public-api:update', ['build.sh'], loadTask('public-api', 'update')); gulp.task('lint', ['format:enforce', 'validate-commit-messages', 'tslint']); gulp.task('tslint', ['tools:build'], loadTask('lint')); gulp.task('validate-commit-messages', loadTask('validate-commit-message')); +gulp.task('source-map-test', loadTask('source-map-test')); gulp.task('tools:build', loadTask('tools-build')); gulp.task('check-cycle', loadTask('check-cycle')); gulp.task('serve', loadTask('serve', 'default')); diff --git a/package.json b/package.json index fdb72a18bd..3c8a5aa65d 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "tslint-eslint-rules": "^3.1.0", "typescript": "^2.4.2", "universal-analytics": "^0.3.9", + "vlq": "^0.2.2", "vrsource-tslint-rules": "^4.0.0", "webpack": "^1.12.6", "xhr2": "^0.1.4", diff --git a/packages/compiler/src/aot/util.ts b/packages/compiler/src/aot/util.ts index 1d02da8d1e..3091011da2 100644 --- a/packages/compiler/src/aot/util.ts +++ b/packages/compiler/src/aot/util.ts @@ -8,7 +8,6 @@ const STRIP_SRC_FILE_SUFFIXES = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; const GENERATED_FILE = /\.ngfactory\.|\.ngsummary\./; -const GENERATED_MODULE = /\.ngfactory$|\.ngsummary$/; const JIT_SUMMARY_FILE = /\.ngsummary\./; const JIT_SUMMARY_NAME = /NgSummary$/; diff --git a/packages/compiler/src/output/abstract_emitter.ts b/packages/compiler/src/output/abstract_emitter.ts index 213c69324b..b8e96b8e9b 100644 --- a/packages/compiler/src/output/abstract_emitter.ts +++ b/packages/compiler/src/output/abstract_emitter.ts @@ -17,8 +17,8 @@ const _INDENT_WITH = ' '; export const CATCH_ERROR_VAR = o.variable('error', null, null); export const CATCH_STACK_VAR = o.variable('stack', null, null); -export abstract class OutputEmitter { - abstract emitStatements( +export interface OutputEmitter { + emitStatements( srcFilePath: string, genFilePath: string, stmts: o.Statement[], preamble?: string|null): string; } diff --git a/packages/compiler/src/util.ts b/packages/compiler/src/util.ts index 8ca94c62f8..20fdb5b95d 100644 --- a/packages/compiler/src/util.ts +++ b/packages/compiler/src/util.ts @@ -9,13 +9,8 @@ import * as o from './output/output_ast'; import {ParseError} from './parse_util'; -const CAMEL_CASE_REGEXP = /([A-Z])/g; const DASH_CASE_REGEXP = /-+([a-z0-9])/g; -export function camelCaseToDashCase(input: string): string { - return input.replace(CAMEL_CASE_REGEXP, (...m: any[]) => '-' + m[1].toLowerCase()); -} - export function dashCaseToCamelCase(input: string): string { return input.replace(DASH_CASE_REGEXP, (...m: any[]) => m[1].toUpperCase()); } diff --git a/packages/compiler/src/view_compiler/view_compiler.ts b/packages/compiler/src/view_compiler/view_compiler.ts index fbf3d80710..9c2ae90789 100644 --- a/packages/compiler/src/view_compiler/view_compiler.ts +++ b/packages/compiler/src/view_compiler/view_compiler.ts @@ -27,7 +27,6 @@ import {componentFactoryResolverProviderDef, depDef, lifecycleHookToNodeFlag, pr const CLASS_ATTR = 'class'; const STYLE_ATTR = 'style'; const IMPLICIT_TEMPLATE_VAR = '\$implicit'; -const NG_CONTAINER_TAG = 'ng-container'; export class ViewCompileResult { constructor(public viewClassVar: string, public rendererTypeVar: string) {} diff --git a/scripts/ci/test-e2e-2.sh b/scripts/ci/test-e2e-2.sh index 65fb7f6e1b..5a2a6f0ac5 100755 --- a/scripts/ci/test-e2e-2.sh +++ b/scripts/ci/test-e2e-2.sh @@ -27,3 +27,7 @@ travisFoldEnd "test.e2e.offlineCompiler" travisFoldStart "test.e2e.platform-server" ./packages/platform-server/integrationtest/run_tests.sh travisFoldEnd "test.e2e.platform-server" + +travisFoldStart "test.e2e.source-maps" + ./node_modules/.bin/gulp source-map-test +travisFoldEnd "test.e2e.source-maps" diff --git a/tools/gulp-tasks/source-map-test.js b/tools/gulp-tasks/source-map-test.js new file mode 100644 index 0000000000..a4982f3525 --- /dev/null +++ b/tools/gulp-tasks/source-map-test.js @@ -0,0 +1,22 @@ +const fs = require('fs'); +const path = require('path'); +const sourceMapTest = require('../source-map-test'); + +const excludedPackages = ['bazel', 'tsc-wrapped', 'benchpress', 'compiler-cli', 'language-service']; + +module.exports = (gulp) => () => { + const packageDir = path.resolve(process.cwd(), 'dist/packages-dist/'); + const packages = + fs.readdirSync(packageDir).filter(package => excludedPackages.indexOf(package) === -1); + + packages.forEach(package => { + if (sourceMapTest(package).length) { + process.exit(1); + } + }); + + if (!packages.length) { + console.log('No packages found in packages-dist. Unable to run source map test.'); + process.exit(1); + } +}; diff --git a/tools/source-map-test/index.js b/tools/source-map-test/index.js new file mode 100644 index 0000000000..d57d93c797 --- /dev/null +++ b/tools/source-map-test/index.js @@ -0,0 +1,66 @@ +#!/usr/bin/env node + +const path = require('path'); +const getMappings = require('./parseMap'); + +const CLOSURE_REGEX = /tsickle_Closure_declarations\(\)/g; +const VAR_REGEX = /(export )?(var (\S+) =)/g; +const FUNCTION_REGEX = /(export )?function (\S+)\((.*)\) {/g; +const CLASS_REGEX = /var (\S+) = \(function \((\S*)\) {/g; +const PROPERTY_REGEX = /Object.defineProperty\((\S+)\.prototype, "(\S+)", {/g; +const METHOD_REGEX = /(\S+)\.prototype\.(\S+) = function \((\S*)\) {/g; +const GETTER_REGEX = /get_REGEX = function \((\S*)\) {/g; +const TYPE_COMMENT_REGEX = /\/\*\* @type {\?} \*\/ /g; +const AFTER_EQUALS_REGEX = /(.+)=(.*)/g; +const EXPORT_REGEX = /export /g; +const TSLIB_REGEX = /tslib_\d\.__/g; +const STRIP_PREFIX_REGEX = /ɵ/g; +const STRIP_SUFFIX_REGEX = /([^$]+)(\$)+\d/g; + +module.exports = function sourceMapTest(package) { + const mappings = + getMappings(getBundlePath(package)).filter(mapping => shouldCheckMapping(mapping.sourceText)); + + console.log(`Analyzing ${mappings.length} mappings for ${package}...`); + + const failures = mappings.filter( + mapping => { return cleanSource(mapping.sourceText) !== cleanGen(mapping.genText); }); + + logResults(failures); + return failures; +}; + +function shouldCheckMapping(text) { + // tsickle closure declaration does not exist in final bundle, so can't be checked + if (CLOSURE_REGEX.test(text)) return false; + return VAR_REGEX.test(text) || FUNCTION_REGEX.test(text) || CLASS_REGEX.test(text) || + PROPERTY_REGEX.test(text) || METHOD_REGEX.test(text) || GETTER_REGEX.test(text); +} + +function cleanSource(source) { + return source.replace(TYPE_COMMENT_REGEX, '') + .replace(EXPORT_REGEX, '') + .replace(STRIP_PREFIX_REGEX, '') + .replace(TSLIB_REGEX, '__') + .replace(AFTER_EQUALS_REGEX, '$1='); +} + +function cleanGen(gen) { + return gen.replace(TYPE_COMMENT_REGEX, '') + .replace(STRIP_PREFIX_REGEX, '') + .replace(STRIP_SUFFIX_REGEX, '$1') + .replace(AFTER_EQUALS_REGEX, '$1='); +} + +function logResults(failures) { + if (failures.length) { + console.error(`... and source maps appear to be broken: ${failures.length} failures.`); + failures.forEach(failure => console.error(failure)); + } else { + console.log('... and source maps look good! 100% match'); + } +} + +function getBundlePath(package) { + return path.resolve(process.cwd(), 'dist/packages-dist/', package, 'esm5/index.js'); +} diff --git a/tools/source-map-test/parseMap.js b/tools/source-map-test/parseMap.js new file mode 100644 index 0000000000..898fbebc24 --- /dev/null +++ b/tools/source-map-test/parseMap.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node + +const vlq = require('vlq'); +const fs = require('fs'); +const path = require('path'); + +module.exports = + function getMappings(bundlePath) { + const sourceMap = JSON.parse(getFile(`${bundlePath}.map`)); + const sourcesContent = sourceMap.sourcesContent.map(file => file.split('\n')); + const bundleLines = getFile(bundlePath).split('\n'); + + let sourceLines = sourcesContent[0]; + let sourceFileIndex = 0; + let sourceLineIndex = 0; + let sourceColIndex = 0; + + return decodeLines(sourceMap).reduce((matchData, mapLine, genLineIndex) => { + mapLine.forEach((segment, index) => { + if (segment.length) { + const [genColDiff, sourceFileDiff, sourceLineDiff, sourceColDiff] = segment; + + // if source file changes, grab new file from sourcesContent + if (sourceFileDiff !== 0) { + sourceFileIndex += sourceFileDiff; + sourceLines = sourcesContent[sourceFileIndex]; + } + + const genText = bundleLines[genLineIndex].trim(); + const sourceText = sourceLines[sourceLineIndex + sourceLineDiff].trim(); + + // only record mappings that are long enough to be meaningful + if (index === 0 && genText.length > 15 && sourceText.length > 15) { + matchData.push({ + genLineIndex, + sourceLineIndex, + sourceFile: sourceMap.sources[sourceFileIndex], genText, sourceText + }); + } + + sourceLineIndex += sourceLineDiff; + sourceColIndex += sourceColDiff; + } + }); + + return matchData; + }, []); +} + +function getFile(filePath) { + return fs.readFileSync(path.resolve(process.cwd(), filePath), 'UTF-8'); +} + +function decodeLines(sourceMap) { + return sourceMap.mappings.split(';').map( + line => { return line.split(',').map(seg => vlq.decode(seg)); }); +}