/** * espower-source - Power Assert instrumentor from source to source. * * https://github.com/power-assert-js/espower-source * * Copyright (c) 2014-2018 Takuto Wada * Licensed under the MIT license. * https://github.com/power-assert-js/espower-source/blob/master/MIT-LICENSE.txt */ 'use strict'; var espower = require('espower'); var acorn = require('acorn'); require('acorn-es7-plugin')(acorn); var estraverse = require('estraverse'); var mergeVisitors = require('merge-estraverse-visitors'); var empowerAssert = require('empower-assert'); var escodegen = require('escodegen'); var extend = require('xtend'); var convert = require('convert-source-map'); var transfer = require('multi-stage-sourcemap').transfer; var _path = require('path'); var isAbsolute = require('path-is-absolute'); function mergeSourceMap (incomingSourceMap, outgoingSourceMap) { if (typeof outgoingSourceMap === 'string' || outgoingSourceMap instanceof String) { outgoingSourceMap = JSON.parse(outgoingSourceMap); } if (!incomingSourceMap) { return outgoingSourceMap; } return JSON.parse(transfer({fromSourceMap: outgoingSourceMap, toSourceMap: incomingSourceMap})); } function copyPropertyIfExists (name, from, to) { if (from[name]) { to.setProperty(name, from[name]); } } function reconnectSourceMap (inMap, outMap) { var mergedRawMap = mergeSourceMap(inMap, outMap.toObject()); var reMap = convert.fromObject(mergedRawMap); copyPropertyIfExists('sources', inMap, reMap); copyPropertyIfExists('sourceRoot', inMap, reMap); copyPropertyIfExists('sourcesContent', inMap, reMap); return reMap; } function handleIncomingSourceMap (originalCode, options) { var inMap; if (options.sourceMap) { if (typeof options.sourceMap === 'string' || options.sourceMap instanceof String) { options.sourceMap = JSON.parse(options.sourceMap); } inMap = options.sourceMap; } else { var sourceMappingURL = retrieveSourceMapURL(originalCode); var commented; // relative file sourceMap // //# sourceMappingURL=foo.js.map or /*# sourceMappingURL=foo.js.map */ if (sourceMappingURL && !/^data:application\/json[^,]+base64,/.test(sourceMappingURL)) { commented = convert.fromMapFileSource(originalCode, _path.dirname(options.path)); } else { // inline sourceMap or none sourceMap commented = convert.fromSource(originalCode); } if (commented) { inMap = commented.toObject(); options.sourceMap = inMap; } } return inMap; } // copy from https://github.com/evanw/node-source-map-support/blob/master/source-map-support.js#L99 function retrieveSourceMapURL(source) { // //# sourceMappingURL=foo.js.map /*# sourceMappingURL=foo.js.map */ var re = /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^\*]+?)[ \t]*(?:\*\/)[ \t]*$)/mg; // Keep executing the search to find the *last* sourceMappingURL to avoid // picking up sourceMappingURLs from comments, strings, etc. var lastMatch, match; while (match = re.exec(source)) { lastMatch = match; } if (!lastMatch) { return null; } return lastMatch[1]; }; function adjustFilepath (filepath, sourceRoot) { if (!sourceRoot || !isAbsolute(filepath)) { return filepath; } return _path.relative(sourceRoot, filepath); } function syntaxErrorLine(lineNumber, maxLineNumberLength, line) { var content = [' ']; var lineNumberString = String(lineNumber); for (var i = lineNumberString.length; i < maxLineNumberLength; i++) { content.push(' '); } content.push(lineNumberString, ': ', line); return content.join(''); } function showSyntaxErrorDetail(code, error, filepath) { var i; var begin = code.lastIndexOf('\n', error.pos); var end = code.indexOf('\n', error.pos); if (end === -1) { end = undefined; } var line = code.slice(begin + 1, end); var beforeLines = []; for (i = 0; i < 5; i++) { if (begin === -1) { break; } var lastBegin = begin; begin = code.lastIndexOf('\n', begin - 1); beforeLines.unshift(code.slice(begin + 1, lastBegin)); if (begin === 0) { break; } } var afterLines = []; for (i = 0; i < 5; i++) { if (end === undefined) { break; } var lastEnd = end; end = code.indexOf('\n', end + 1); if (end === -1) { end = undefined; } afterLines.push(code.slice(lastEnd + 1, end)); } var lines = ['']; var numberLength = String(error.loc.line + afterLines.length).length; for (i = 0; i < beforeLines.length; i++) { lines.push( syntaxErrorLine(error.loc.line - beforeLines.length + i, numberLength, beforeLines[i]) ); } lines.push(syntaxErrorLine(error.loc.line, numberLength, line)); var lineContent = []; for (i = 0; i < 6 + numberLength + error.loc.column - 1; i++) { lineContent.push(' '); } lineContent.push('^'); lines.push(lineContent.join('')); for (i = 0; i < afterLines.length; i++) { lines.push( syntaxErrorLine(error.loc.line + i + 1, numberLength, afterLines[i]) ); } lines.push('', 'Parse Error: ' + error.message + (filepath ? ' in ' + filepath : '')); var detail = lines.join('\n'); var err = new SyntaxError(detail); err.loc = error.loc; err.pos = error.pos; err.raisedAt = error.pos; throw err; } function instrument (originalCode, filepath, options) { var jsAst; try { jsAst = acorn.parse(originalCode, {locations: true, ecmaVersion: options.ecmaVersion, sourceType: options.sourceType, plugins: {asyncawait: true}}); } catch (e) { if (e instanceof SyntaxError && e.pos && e.loc) { showSyntaxErrorDetail(originalCode, e, filepath); } throw e; } var modifiedAst = estraverse.replace(jsAst, mergeVisitors([ { enter: empowerAssert.enter }, espower.createVisitor(jsAst, options) ])); var escodegenOptions = extend({ sourceMap: adjustFilepath(filepath || options.path, options.sourceRoot), sourceContent: originalCode, sourceMapWithCode: true }); if (options.sourceRoot) { escodegenOptions.sourceMapRoot = options.sourceRoot; } return escodegen.generate(modifiedAst, escodegenOptions); } function instrumentWithoutSourceMapOutput (originalCode, options) { var jsAst; try { jsAst = acorn.parse(originalCode, {locations: true, ecmaVersion: options.ecmaVersion, sourceType: options.sourceType, plugins: {asyncawait: true}}); } catch (e) { if (e instanceof SyntaxError && e.pos && e.loc) { showSyntaxErrorDetail(originalCode, e); } throw e; } var modifiedAst = estraverse.replace(jsAst, mergeVisitors([ { enter: empowerAssert.enter }, espower.createVisitor(jsAst, options) ])); return escodegen.generate(modifiedAst); } function mergeEspowerOptions (options, filepath) { return extend(espower.defaultOptions(), { ecmaVersion: 2018, sourceType: 'module', path: filepath }, options); } module.exports = function espowerSource (originalCode, filepath, options) { if (typeof originalCode === 'undefined' || originalCode === null) { throw new espower.EspowerError('`originalCode` is not specified', espowerSource); } var espowerOptions = mergeEspowerOptions(options, filepath); var inMap = handleIncomingSourceMap(originalCode, espowerOptions); if (!(filepath || espowerOptions.path)) { return instrumentWithoutSourceMapOutput(originalCode, espowerOptions); } var instrumented = instrument(originalCode, filepath, espowerOptions); var outMap = convert.fromJSON(instrumented.map.toString()); if (inMap) { var reMap = reconnectSourceMap(inMap, outMap); return instrumented.code + '\n' + reMap.toComment() + '\n'; } else { return instrumented.code + '\n' + outMap.toComment() + '\n'; } };