'use strict' // eslint-disable-next-line node/no-deprecated-api const resolve = require('url').resolve const SourceMapConsumer = require('source-map').SourceMapConsumer const _ = require('lodash') const PathUtils = require('./utils/path-utils') const log = require('./logger').create('reporter') const MultiReporter = require('./reporters/multi') const baseReporterDecoratorFactory = require('./reporters/base').decoratorFactory function createErrorFormatter (config, emitter, SourceMapConsumer) { const basePath = config.basePath const urlRoot = config.urlRoot === '/' ? '' : (config.urlRoot || '') let lastServedFiles = [] emitter.on('file_list_modified', (files) => { lastServedFiles = files.served }) const URL_REGEXP = new RegExp('(?:https?:\\/\\/' + config.hostname + '(?:\\:' + config.port + ')?' + ')?\\/?' + urlRoot + '\\/?' + '(base/|absolute)' + // prefix, including slash for base/ to create relative paths. '((?:[A-z]\\:)?[^\\?\\s\\:]*)' + // path '(\\?\\w*)?' + // sha '(\\:(\\d+))?' + // line '(\\:(\\d+))?' + // column '', 'g') const cache = new WeakMap() function getSourceMapConsumer (sourceMap) { if (!cache.has(sourceMap)) { cache.set(sourceMap, new SourceMapConsumer(sourceMap)) } return cache.get(sourceMap) } return function (input, indentation) { indentation = _.isString(indentation) ? indentation : '' if (_.isError(input)) { input = input.message } else if (_.isEmpty(input)) { input = '' } else if (!_.isString(input)) { input = JSON.stringify(input, null, indentation) } let msg = input.replace(URL_REGEXP, function (stackTracePath, prefix, path, __, ___, line, ____, column) { const normalizedPath = prefix === 'base/' ? `${basePath}/${path}` : path const file = lastServedFiles.find((file) => file.path === normalizedPath) if (file && file.sourceMap && line) { line = +line column = +column // When no column is given and we default to 0, it doesn't make sense to only search for smaller // or equal columns in the sourcemap, let's search for equal or greater columns. const bias = column ? SourceMapConsumer.GREATEST_LOWER_BOUND : SourceMapConsumer.LEAST_UPPER_BOUND try { const zeroBasedColumn = Math.max(0, (column || 1) - 1) const original = getSourceMapConsumer(file.sourceMap).originalPositionFor({ line, column: zeroBasedColumn, bias }) // If there is no original position/source for the current stack trace path, then // we return early with the formatted generated position. This handles the case of // generated code which does not map to anything, see Case 1 of the source-map spec. // https://sourcemaps.info/spec.html. if (original.source === null) { return PathUtils.formatPathMapping(path, line, column) } // Source maps often only have a local file name, resolve to turn into a full path if // the path is not absolute yet. const oneBasedOriginalColumn = original.column == null ? original.column : original.column + 1 return `${PathUtils.formatPathMapping(resolve(path, original.source), original.line, oneBasedOriginalColumn)} <- ${PathUtils.formatPathMapping(path, line, column)}` } catch (e) { log.warn(`An unexpected error occurred while resolving the original position for: ${stackTracePath}`) log.warn(e) } } return PathUtils.formatPathMapping(path, line, column) || prefix }) if (indentation) { msg = indentation + msg.replace(/\n/g, '\n' + indentation) } return config.formatError ? config.formatError(msg) : msg + '\n' } } function createReporters (names, config, emitter, injector) { const errorFormatter = createErrorFormatter(config, emitter, SourceMapConsumer) const reporters = [] names.forEach((name) => { if (['dots', 'progress'].includes(name)) { [ require(`./reporters/${name}`), require(`./reporters/${name}_color`) ].forEach((Reporter) => { reporters.push(new Reporter(errorFormatter, config.reportSlowerThan, config.colors, config.browserConsoleLogOptions)) }) return } const locals = { baseReporterDecorator: ['factory', baseReporterDecoratorFactory], formatError: ['value', errorFormatter] } try { log.debug(`Trying to load reporter: ${name}`) reporters.push(injector.createChild([locals], ['reporter:' + name]).get('reporter:' + name)) } catch (e) { if (e.message.includes(`No provider for "reporter:${name}"`)) { log.error(`Can not load reporter "${name}", it is not registered!\n Perhaps you are missing some plugin?`) } else { log.error(`Can not load "${name}"!\n ${e.stack}`) } emitter.emit('load_error', 'reporter', name) return } const colorName = name + '_color' if (!names.includes(colorName)) { try { log.debug(`Trying to load color-version of reporter: ${name} (${colorName})`) reporters.push(injector.createChild([locals], ['reporter:' + colorName]).get('reporter:' + name)) } catch (e) { log.debug('Couldn\'t load color-version.') } } }) reporters.forEach((reporter) => emitter.bind(reporter)) return new MultiReporter(reporters) } createReporters.$inject = [ 'config.reporters', 'config', 'emitter', 'injector' ] exports.createReporters = createReporters