UNPKG

@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
"use strict"; 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;