@ima/dev-utils
Version:
IMA.js dev utils used used mainly in @ima/cli and other dev-related utilities.
201 lines (200 loc) • 8.57 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatError = exports.parseError = exports.getSource = void 0;
const fs_1 = __importDefault(require("fs"));
const chalk_1 = __importDefault(require("chalk"));
const cli_highlight_1 = require("cli-highlight");
const source_map_js_1 = require("source-map-js");
const stackTraceParser = __importStar(require("stacktrace-parser"));
const compileErrorParser_1 = require("./compileErrorParser");
const sourceFragment_1 = require("./sourceFragment");
const sourceMapUtils_1 = require("./sourceMapUtils");
/**
* Get source fragment from provided source metadata.
* Optionally it tries to parse original content if
* source maps are available.
*
* @param {string?} fileUri source file uri.
* @param {number?} line errored line number.
* @param {number?} column errored column number.
* @returns {Promise<string[]>} Formatted error lines.
*/
async function getSource(fileUri, line, column = 0) {
if (!fileUri || typeof line !== 'number') {
return;
}
let sourceLines = [];
const fileContents = await fs_1.default.promises.readFile(fileUri, 'utf8');
const sourceMapUrl = (0, sourceMapUtils_1.extractSourceMappingUrl)(fileUri, fileContents);
// Parse source maps
if (sourceMapUrl) {
// Try to parse original content
if (sourceMapUrl && fs_1.default.existsSync(sourceMapUrl)) {
const rawSourceMap = JSON.parse(await fs_1.default.promises.readFile(sourceMapUrl, 'utf8'));
const sourceMap = await new source_map_js_1.SourceMapConsumer(rawSourceMap);
const orgPosition = sourceMap.originalPositionFor({
column,
line,
});
if (orgPosition.source === null || orgPosition.line === null) {
return [];
}
const orgSource = sourceMap.sourceContentFor(orgPosition.source);
if (!orgSource) {
return [];
}
// Create source fragment for original content
sourceLines = (0, sourceFragment_1.createSourceFragment)(orgPosition.line, orgSource + '', 4);
}
}
// Fallback to bundeled sources
if (sourceLines.length === 0) {
sourceLines = (0, sourceFragment_1.createSourceFragment)(line, fileContents, 4);
}
return sourceLines.map(line => chalk_1.default.gray(` ${line.highlight ? chalk_1.default.red('> ') : ' '}${line.line} | `) +
// Replace tabs with spaces and highlight
(0, cli_highlight_1.highlight)(line.source.replace(/\t/g, ' '), {
language: fileUri?.split('.').pop() ?? 'javascript',
ignoreIllegals: true,
theme: (0, cli_highlight_1.fromJson)({
keyword: 'cyan',
class: 'yellow',
built_in: 'yellow',
function: 'magenta',
string: 'green',
tag: 'gray',
attr: 'cyan',
doctag: 'gray',
comment: 'gray',
deletion: ['red', 'strikethrough'],
regexp: 'yellow',
literal: 'magenta',
number: 'magenta',
attribute: 'red',
}),
}));
}
exports.getSource = getSource;
/**
* Formats provided error object into readable format including
* the errored source code fragment with line highlight. Works
* with runtime and compile errors while trying to show all
* relevant information that can be extracted from provided object.
*
* @param {Error|StatsError} Error object to format.
* @param {string?} type Error type (affects error parsing).
* @param {string?} rootDir Optional root directory used to print
* absolute URLs as relative to the current rootDir.
* @param {string[]?} uniqueTracker Array of error identifiers to
* track uniques, if the error matches identifier already included
* in this array, this function returns empty string.
* @returns {Promise<string>} Formatted error output.
*/
async function parseError(error, type) {
const parsedErrorData = {
name: error.name,
message: error.message,
stack: error.stack,
};
// Try to parse an error
if (type === 'compile') {
const compileError = (0, compileErrorParser_1.parseCompileError)(error);
// Extract parsed parts
parsedErrorData.name = compileError?.name;
parsedErrorData.message = compileError?.message;
parsedErrorData.fileUri = compileError?.fileUri;
parsedErrorData.column = compileError?.column;
parsedErrorData.line = compileError?.line;
return parsedErrorData;
}
if (type === 'runtime' && error.stack) {
const parsedStack = stackTraceParser.parse(error.stack);
// Extract parsed parts from error stack
parsedErrorData.functionName = parsedStack[0].methodName;
parsedErrorData.fileUri = parsedStack[0].file ?? undefined;
parsedErrorData.column = parsedStack[0].column ?? undefined;
parsedErrorData.line = parsedStack[0].lineNumber ?? undefined;
return parsedErrorData;
}
return parsedErrorData;
}
exports.parseError = parseError;
/**
* Formats provided error object into readable format including
* the errored source code fragment with line highlight. Works
* with runtime and compile errors while trying to show all
* relevant information that can be extracted from provided object.
*
* @param {ParsedErrorData} parsedErrorData Parsed error data object
* obtained from parseError function (or provided directly).
* @param {string?} rootDir Optional root directory used to print
* absolute URLs as relative to the current rootDir.
* @param {string[]?} uniqueTracker Array of error identifiers to
* track uniques, if the error matches identifier already included
* in this array, this function returns empty string.
* @returns {Promise<string>} Formatted error output.
*/
async function formatError(parsedErrorData, rootDir, uniqueTracker) {
let { fileUri } = parsedErrorData;
const { column, functionName, line, message, name, stack } = parsedErrorData;
// Normalize fileUri
if (fileUri && rootDir) {
fileUri = fileUri.replace(rootDir, '.');
}
// Get source fragment
const sourceFragment = await getSource(fileUri, line, column);
// Track unique errors
if (Array.isArray(uniqueTracker)) {
const errorIdentifier = `${fileUri}:${line}:${column}`;
// Return empty string for already processed errors
if (uniqueTracker.includes(errorIdentifier)) {
return '';
}
else {
uniqueTracker.push(errorIdentifier);
}
}
// Assemble error message
return [
fileUri &&
[
functionName && `${chalk_1.default.magenta(`${functionName}`)} at`,
chalk_1.default.underline.bold.blueBright(fileUri) + `:${line}:${column}`,
]
.filter(Boolean)
.join(' '),
name && `${chalk_1.default.redBright(`${name}:`)} ` + message,
...(sourceFragment ? ['', ...sourceFragment] : []),
stack && `\n${chalk_1.default.gray(stack.replace('', ''))}`,
'', // Empty line
]
.filter(value => value === '' || !!value)
.join('\n');
}
exports.formatError = formatError;