You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

761 lines
33 KiB

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (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.Formatter = void 0;
const code_frame_1 = require("@babel/code-frame");
const chalk_1 = __importDefault(require("chalk"));
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const Matchers_1 = require("./Matchers");
const Parser_1 = require("./Parser");
const switchRegex_1 = require("./switchRegex");
const symbols_1 = require("./utils/symbols");
var Status;
(function (Status) {
Status["Pass"] = "pass";
Status["Fail"] = "fail";
Status["Pending"] = "pending";
Status["Error"] = "error";
Status["Completion"] = "completion";
Status["Measure"] = "measure";
})(Status || (Status = {}));
function highlightLastPathComponent(filePath) {
return chalk_1.default.dim(path.dirname(filePath) + '/') + path.basename(filePath);
}
function format(command, argumentText = '', success = true) {
const symbol = statusSymbol(success ? Status.Completion : Status.Fail);
return [symbol, chalk_1.default.bold(command), argumentText].join(' ').trim();
}
function formatTest(testCase, status) {
return [statusSymbol(status), testCase].join(' ').trim();
}
function heading(prefix, text, description) {
return [prefix, chalk_1.default.white(text), description].join(' ').trim();
}
function statusSymbol(status) {
switch (status) {
case Status.Pass:
return chalk_1.default.green(symbols_1.PASS);
case Status.Fail:
return chalk_1.default.red(symbols_1.FAIL);
case Status.Pending:
return chalk_1.default.cyan(symbols_1.PENDING);
case Status.Error:
return chalk_1.default.red(symbols_1.ERROR);
case Status.Completion:
return chalk_1.default.white(symbols_1.COMPLETION);
case Status.Measure:
return chalk_1.default.magenta(symbols_1.MEASURE);
default:
return '';
}
}
function coloredTime(time) {
const flt = parseFloat(time);
if (flt >= 0 && flt <= 0.025) {
return time;
}
else if (flt >= 0.026 && flt <= 0.1) {
return chalk_1.default.yellow(time);
}
return chalk_1.default.red(time);
}
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function relativePath(projectRoot, filePath) {
return slash(path.relative(projectRoot, filePath));
}
function formatBreadCrumb(fileName, target, project) {
// TODO: Simplify
return [project === target ? project : [project, target].filter(Boolean).join(`/`), fileName]
.filter(Boolean)
.join(` ${symbols_1.BREADCRUMB} `);
}
function getAppRoot(filePath) {
let appRoot = filePath;
const isBuildFolder = (path) => /((Debug|Release)-[^/\s\d]*$)|(.app$)/.test(path);
while (!isBuildFolder(appRoot) && appRoot.length > 1) {
appRoot = path.dirname(appRoot);
}
return isBuildFolder(appRoot) ? appRoot : '';
}
class Formatter {
constructor(props) {
this.props = props;
this.errors = [];
this.warnings = [];
}
get parser() {
if (this._parser) {
return this._parser;
}
this._parser = new Parser_1.Parser(this);
return this._parser;
}
pipe(data) {
const lines = [];
data.split(os.EOL).forEach(line => {
const results = this.parser.parse(line);
if (typeof results === 'string' && results.trim()) {
lines.push(results);
}
});
return lines;
}
dimConfiguration(configuration) {
return chalk_1.default.dim(`(${configuration})`);
}
getTitleForConfigurationType(type) {
switch (type) {
case 'Clean':
return 'Cleaning';
case 'Aggregate':
return 'Aggregate';
case 'Analyze':
return 'Analyzing';
case 'Build':
return 'Building';
default:
return 'Unknown';
}
}
formatTarget(props) {
return format(this.getTitleForConfigurationType(props.type), this.dimConfiguration(formatBreadCrumb(props.configuration, props.target, props.project)));
}
formatCopy({ from, to }) {
const relativeFile = relativePath(this.props.projectRoot, from);
const appFileRoot = getAppRoot(to);
const relativeAppFile = relativePath(appFileRoot, to);
return format('Copying ', [relativeFile, relativeAppFile].join(' ➜ '));
}
getFileOperationTitle(type) {
switch (type) {
case 'Analyze':
return 'Analyzing';
case 'GenerateDSYMFile':
return `Generating debug`;
case 'Ld':
return 'Linking ';
case 'Libtool':
return 'Packaging';
case 'ProcessPCH':
return 'Precompiling';
case 'ProcessInfoPlistFile':
return 'Preparing';
case 'CodeSign':
return 'Signing ';
case 'Touch':
return 'Creating ';
case 'CompileC':
case 'CompileSwift':
case 'CompileXIB':
case 'CompileStoryboard':
return 'Compiling';
default:
// Unknown file operation
return '';
}
}
formatFileOperation(props) {
const title = this.getFileOperationTitle(props.type);
switch (props.type) {
case 'Analyze':
return format(title, formatBreadCrumb(props.fileName, props.target, props.project));
case 'GenerateDSYMFile':
return format(title, formatBreadCrumb(`'${props.fileName}'`, props.target, props.project));
case 'Ld':
return format(title, formatBreadCrumb(props.fileName, props.target, props.project));
case 'Libtool':
return format(title, formatBreadCrumb(props.fileName, props.target, props.project));
case 'ProcessPCH':
return format(title, formatBreadCrumb(props.fileName, props.target, props.project));
case 'ProcessInfoPlistFile':
return format(title, formatBreadCrumb(props.fileName, props.target, props.project));
case 'CodeSign':
return format(title, formatBreadCrumb(props.fileName, props.target, props.project));
case 'Touch':
return format(title, props.fileName);
case 'CompileC':
case 'CompileSwift':
case 'CompileXIB':
case 'CompileStoryboard':
return format(title, formatBreadCrumb(props.fileName, props.target, props.project));
default:
// Unknown file operation
return '';
}
}
formatPhaseSuccess(phaseName, duration) {
return format(capitalize(phaseName.toLowerCase()), `Succeeded${duration ? ` (${duration})` : ''}`, true);
}
formatPhaseScriptExecution(scriptName, target, project) {
// TODO: Track (scriptName === '[CP] Copy XCFrameworks')
return format('Executing', formatBreadCrumb(`'${scriptName}'`, target, project));
}
formatPreprocess(file) {
return format('Preprocessing', file);
}
formatShellCommand(command, args) {
return '';
}
formatCompileCommand(compilerCommand, filePath) {
return '';
}
formatProcessPchCommand(filePath) {
return '';
}
formatWriteFile(file) {
return '';
}
formatOther(text) {
return '';
}
formatSingleLineCompileIssue(type, filePathAndLocation, fileName, reason, target, project) {
// Allow disabling this warning.
const { filePath, lineNumber, columnNumber } = splitPathInfo(filePathAndLocation);
if (type === 'warning') {
if (!this.shouldShowCompileWarning(filePath, lineNumber, columnNumber)) {
return '';
}
}
// Prevent `/foo/bar:1:1` instead using `/foo/bar` since it's a bit more focused.
const sanitizedFilePath = lineNumber === '1' && columnNumber === '1' ? filePath : filePathAndLocation;
// Get the `Project/Target` prefix.
const packageName = [project, target].join('/');
// Choose a color.
const color = type === 'warning' ? chalk_1.default.yellow : chalk_1.default.red;
const platform = color.bold(`${packageName}:`);
// Choose a symbol.
const symbol = type === 'warning' ? symbols_1.WARNING : symbols_1.ERROR;
// Get a more concise file path when the issue is inside the build folder.
const appFileRoot = getAppRoot(sanitizedFilePath);
const relativeAppFile = appFileRoot
? chalk_1.default.gray('[app]/') + relativePath(appFileRoot, sanitizedFilePath)
: sanitizedFilePath;
// Create the message.
const results = chalk_1.default `${symbol} ${platform} ${reason.trim()}\n {gray └─${relativeAppFile}}`;
// Ensure we track the message
if (type === 'warning') {
this.warnings.push(results);
}
else {
this.errors.push(results);
}
return results;
}
// These are like comments but for Xcode logs.
formatRemark(msg) {
return '';
}
formatEmitSwiftModule(type, arch, target, project) {
return '';
}
formatCompileSwiftSources(type, arch, pkg, target, project) {
return '';
}
formatCleanRemove(msg) {
return '';
}
formatWriteAuxiliaryFiles(text) {
return '';
}
formatTiffutil(file) {
return format('Validating', file);
}
formatCheckDependencies(text) {
return format('Check Dependencies');
}
formatWillNotBeCodeSigned(message) {
const results = `${chalk_1.default.yellow(symbols_1.WARNING + ' ' + message)}`;
this.warnings.push(results);
return results;
}
// COMPILER / LINKER ERRORS AND WARNINGS
/**
*
* @param fileName 'SampleTest.m',
* @param filePathAndLocation '/Users/foo/bar.m:12:59',
* @param reason 'expected identifier',
* @param line ' [[thread should] equal:thread.];',
* @param cursor ' ^'
*/
formatCompileError(fileName, filePathAndLocation, reason, line, cursor) {
const { filePath, lineNumber, columnNumber } = splitPathInfo(filePathAndLocation);
const results = formatWarningOrError({
isError: true,
filePath,
reason,
cursor,
lineText: line,
lineNumber,
columnNumber,
projectRoot: this.props.projectRoot,
maxWarningLineLength: this.props.maxWarningLineLength,
});
this.errors.push(results);
return results;
}
formatError(message) {
const results = switchRegex_1.switchRegex(message, [
[
Matchers_1.Matchers.Errors.UNSUPPORTED_ENTITLEMENT_MATCHER,
([, $1, $2, $3, $4, $5]) => {
return this.formatUnsupportedEntitlementError($1, $2, $3, $4, $5);
},
],
[null, () => this.formatGenericError(message)],
]);
this.errors.push(results);
return results;
}
/**
* In: `error: Provisioning profile "iOS Team Provisioning Profile: *" doesn't support the Push Notifications capability. (in target 'yolo90' from project 'yolo90')`
* Out: `❌ yolo90/yolo90: Provisioning Profile "iOS Team Provisioning Profile: *" does not support the Push Notifications capability.`
*
* In: `error: Provisioning profile "iOS Team Provisioning Profile: *" doesn't include the aps-environment entitlement. (in target 'yolo90' from project 'yolo90')`
* Out: `❌ yolo90/yolo90: Entitlements file defines the value "aps-environment" which is not registered for profile "iOS Team Provisioning Profile: *".`
*
* @param profileName `"iOS Team Provisioning Profile: *"`
* @param entitlementName `Push Notifications` | `aps-environment`
* @param entitlementType `capability` | `entitlement`
* @param target boost-for-react-native
* @param project Pods
*/
formatUnsupportedEntitlementError(profileName, entitlementName, entitlementType, target, project) {
const packageName = [project, target].join('/');
const platform = chalk_1.default.red.bold(`${packageName}:`);
if (entitlementType === 'capability') {
return chalk_1.default `${symbols_1.ERROR} ${platform} Provisioning Profile ${profileName} does not support the {red ${entitlementName}} capability.`;
}
return chalk_1.default `${symbols_1.ERROR} ${platform} Entitlements file defines the value {red "${entitlementName}"} which is not registered for profile ${profileName}.`;
}
formatFileMissingError(reason, filePath) {
const results = `\n${chalk_1.default.red(symbols_1.ERROR + ' ' + reason)} ${filePath}\n\n`;
this.errors.push(results);
return results;
}
formatLdWarning(reason) {
const results = switchRegex_1.switchRegex(reason, [
[
Matchers_1.Matchers.Warnings.LINKER_METHOD_OVERRIDE,
([, $1, $2, $3, $4, $5]) => {
return this.formatLdMethodOverride($1, [
{ filePath: $2, name: $3 },
{ filePath: $4, name: $5 },
]);
},
],
[null, () => `${chalk_1.default.yellow(symbols_1.WARNING + ' ' + reason)}`],
]);
this.warnings.push(results);
return results;
}
formatUndefinedSymbols(message, symbol, reference) {
const symbols = chalk_1.default.gray(`┌─ Symbol: ${symbol}\n└─ Referenced from: ${reference}`);
const results = `${chalk_1.default.red(symbols_1.ERROR + ' ' + message)}\n${symbols}\n`;
this.errors.push(results);
return results;
}
formatLdMethodOverride(methodName, collisions) {
const formattedMessage = chalk_1.default.yellow(symbols_1.WARNING + ` ld: duplicate method '${chalk_1.default.bold(methodName)}' in`);
const types = ['category', 'class'];
const symbols = chalk_1.default.gray(collisions
.map(({ filePath, name }, i) => {
const appFileRoot = getAppRoot(filePath);
const relativeAppFile = relativePath(appFileRoot, filePath);
const branch = i === collisions.length - 1 ? '└─' : i === 0 ? '┌─' : '├─';
return `${branch}${`[${types[i]}]`}: ${name} ${chalk_1.default.dim(relativeAppFile)}`;
})
.join('\n'));
return `${formattedMessage}\n${symbols}\n`;
}
formatDuplicateSymbols(message, filePaths, isWarning) {
const formattedMessage = isWarning
? chalk_1.default.yellow(symbols_1.WARNING + ' ' + message)
: chalk_1.default.red(symbols_1.ERROR + ' ' + message);
const symbols = chalk_1.default.gray(filePaths
.map((p, i) => {
const branch = i === filePaths.length - 1 ? '└─' : i === 0 ? '┌─' : '├─';
return `${branch} ${path.basename(p)}`;
})
.join('\n'));
const results = `${formattedMessage}\n${symbols}\n`;
if (isWarning) {
this.warnings.push(results);
}
else {
this.errors.push(results);
}
return results;
}
/**
* In: `The iOS Simulator deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0, but the range of supported deployment target versions is 9.0 to 14.3.99. (in target 'boost-for-react-native' from project 'Pods')`
* Out: `⚠️ Pods/boost-for-react-native: iOS@8.0 version mismatch. Expected >= 9.0 < 14.3.99`
*
* @param os iOS
* @param deploymentTarget IPHONEOS_DEPLOYMENT_TARGET
* @param version 8.0
* @param minVersion 9.0
* @param maxVersion 14.3.99
* @param target boost-for-react-native
* @param project Pods
*/
formatVersionMismatchWarning(os, deploymentTarget, version, minVersion, maxVersion, target, project) {
const packageName = [project, target].join('/');
const platform = chalk_1.default.bold(`${packageName}:`);
const packageNameWithVersion = chalk_1.default.greenBright(os) + chalk_1.default.cyan `@` + chalk_1.default.magenta(version);
const expectedRange = `>= ${minVersion} <= ${maxVersion}`;
return `${symbols_1.WARNING} ${platform} ${packageNameWithVersion} deployment version mismatch, expected ${expectedRange}`;
}
/**
* In: `warning: [CP] Vendored binary '/Users/evanbacon/Library/Developer/Xcode/DerivedData/yolo67-hcjsxsdqyxnsgdednlbpylgeffja/Build/Intermediates.noindex/Pods.build/Debug-iphonesimulator/hermes-engine.build/DerivedSources/hermes.framework.dSYM/Contents/Resources/DWARF/hermes' contains architectures (armv7 armv7s arm64) none of which match the current build architectures (x86_64).`
* Out: `⚠️ Vendored binary '[app]/hermes-engine.build/DerivedSources/hermes.framework.dSYM/Contents/Resources/DWARF/hermes' does not support current build architecture (x86_64). Supported architectures: armv7, armv7s, arm64.`
*
* @param os iOS
* @param deploymentTarget IPHONEOS_DEPLOYMENT_TARGET
* @param version 8.0
* @param minVersion 9.0
* @param maxVersion 14.3.99
* @param target boost-for-react-native
* @param project Pods
*/
formatMissingArchitectureWarning(binaryPath, architectures, currentArchitectures) {
const appFileRoot = getAppRoot(binaryPath);
const relativeAppFile = appFileRoot
? chalk_1.default.gray('[app]/') + relativePath(appFileRoot, binaryPath)
: binaryPath;
const architectureString = currentArchitectures.length === 1 ? 'architecture' : 'architectures';
const supportedString = chalk_1.default.dim(`Supported architectures: ${architectures.join(', ')}.`);
return (chalk_1.default.yellow(`${symbols_1.WARNING} Vendored binary '${relativeAppFile}' does not support current build ${architectureString} (${chalk_1.default.bold(currentArchitectures.join(', '))}). `) + supportedString);
}
/**
* In: `Skipping duplicate build file in Compile Sources build phase: /Users/evanbacon/Documents/GitHub/expo/ios/Exponent/Kernel/ReactAppManager/EXReactAppManager.mm (in target 'Exponent' from project 'Exponent')`
* Out:
* `⚠️ Skipping duplicate file: Exponent/Kernel/ReactAppManager/EXReactAppManager.mm:
* Remove: Exponent » Exponent » Build Phases » Compile Sources » EXReactAppManager.mm`
*
* @param filePath
* @param buildPhase 'Compile Sources'
* @param target Exponent-watch-app
* @param project Exponent
*/
formatDuplicateFileCompilerWarning(filePath, buildPhase, target, project) {
const message = `${chalk_1.default.yellow `Skipping duplicate file:`} ${relativePath(this.props.projectRoot, filePath)}`;
const fileName = path.basename(filePath);
const crumbs = chalk_1.default.gray('Remove: ' +
['Xcode', `${project}/${target}`, 'Build Phases', buildPhase, fileName].join(` ${symbols_1.BREADCRUMB} `));
return `${symbols_1.WARNING} ${message}\n ${crumbs}\n`;
}
/**
* In: `The Copy Bundle Resources build phase contains this target's Info.plist file '/Users/evanbacon/Documents/GitHub/expo/ios/Exponent/Supporting/Info.plist'. (in target 'Exponent' from project 'Exponent')`
* Out:
* `⚠️ Target's Info.plist file is incorrectly linked: Exponent/Supporting/Info.plist:
* Remove: Exponent » Exponent » Build Phases » Copy Bundle Resources » Info.plist`
*
* @param filePath
* @param reservedFileDescription 'entitlements'
* @param target Exponent-watch-app
* @param project Exponent
*/
formatReservedFileInCopyBundleResourcesCompilerWarning(filePath, reservedFileDescription, target, project) {
const message = `${chalk_1.default.yellow `Target's ${chalk_1.default.bold(reservedFileDescription)} file is incorrectly linked:`} ${relativePath(this.props.projectRoot, filePath)}`;
const fileName = path.basename(filePath);
const crumbs = chalk_1.default.gray('Remove: ' +
['Xcode', `${project}/${target}`, 'Build Phases', 'Copy Bundle Resources', fileName].join(` ${symbols_1.BREADCRUMB} `));
return `${symbols_1.WARNING} ${message}\n ${crumbs}\n`;
}
formatMissingFileCompilerWarning(filePath) {
return `${symbols_1.WARNING} ${chalk_1.default.yellow `No such file or directory:`} ${filePath}`;
}
formatGenericError(message) {
return `\n${chalk_1.default.red(symbols_1.ERROR + ' ' + message)}\n\n`;
}
formatGenericWarning(message) {
return symbols_1.INDENT + chalk_1.default.yellow(message);
}
formatWarning(message) {
const results = switchRegex_1.switchRegex(message, [
[
Matchers_1.Matchers.Warnings.MISSING_ARCHITECTURE,
([, $1, $2, $3]) => {
return this.formatMissingArchitectureWarning($1, $2 === null || $2 === void 0 ? void 0 : $2.split(' ').map(value => value.trim()), $3 === null || $3 === void 0 ? void 0 : $3.split(' ').map(value => value.trim()));
},
],
[
Matchers_1.Matchers.Warnings.VERSION_MISMATCH,
([, $1, $2, $3, $4, $5, $6, $7]) => {
return this.formatVersionMismatchWarning($1, $2, $3, $4, $5, $6, $7);
},
],
[
Matchers_1.Matchers.Warnings.MISSING_FILE_COMPILER_WARNING_MATCHER,
([, $1]) => {
return this.formatMissingFileCompilerWarning($1);
},
],
[
Matchers_1.Matchers.Warnings.SKIPPING_DUPLICATE_FILE,
([, $1, $2, $3, $4]) => {
return this.formatDuplicateFileCompilerWarning($2, $1, $3, $4);
},
],
[
Matchers_1.Matchers.Warnings.TARGETS_FILE_INCLUDED,
([, $1, $2, $3, $4]) => {
return this.formatReservedFileInCopyBundleResourcesCompilerWarning($2, $1, $3, $4);
},
],
[null, () => this.formatGenericWarning(message)],
]);
this.warnings.push(results);
return results;
}
// TODO: see how we can unify formatError and formatCompileError,
// the same for warnings
formatCompileWarning(fileName, filePathAndLocation, reason, line, cursor) {
const { filePath, lineNumber, columnNumber } = splitPathInfo(filePathAndLocation);
if (this.shouldShowCompileWarning(filePath, lineNumber, columnNumber)) {
const results = formatWarningOrError({
isError: false,
filePath,
reason,
cursor,
lineText: line,
lineNumber,
columnNumber,
projectRoot: this.props.projectRoot,
maxWarningLineLength: this.props.maxWarningLineLength,
});
this.warnings.push(results);
return results;
}
return '';
}
shouldShowCompileWarning(filePath, lineNumber, columnNumber) {
return true;
}
formatPendingTest(suite, test) {
return symbols_1.INDENT + formatTest(`${test} [PENDING]`, Status.Pending);
}
formatPassingTest(suite, test, time) {
return symbols_1.INDENT + formatTest(`${test} (${coloredTime(time)} seconds)`, Status.Pass);
}
formatMeasuringTest(suite, test, time) {
return symbols_1.INDENT + formatTest(`${test} measured (${coloredTime(time)} seconds)`, Status.Measure);
}
formatFailingTest(suite, test, reason, filePath) {
return symbols_1.INDENT + formatTest(`${test}, ${reason}`, Status.Fail);
}
formatTestRunStarted(name) {
return heading('Test Suite', name, 'started');
}
formatTestSuiteStarted(name) {
return heading('', name, '');
}
formatTestRunFinished(name, time) {
return '';
}
// Will be printed by default. Override with '' if you don't want summary
formatTestSummary(executedMessage, failuresPerSuite) {
const failures = this.formatFailures(failuresPerSuite);
let finalMessage = '';
if (!failures) {
finalMessage = chalk_1.default.green(executedMessage);
}
else {
finalMessage = chalk_1.default.red(executedMessage);
}
const text = [failures, finalMessage].join('\n\n\n').trim();
return `\n\n${text}`;
}
formatFailures(failuresPerSuite) {
return Object.entries(failuresPerSuite)
.map(([suite, failures]) => {
const formattedFailures = failures.map(failure => this.formatFailure(failure)).join('\n\n');
return `\n${suite}\n${formattedFailures}`;
})
.join('\n');
}
formatFailure(f) {
const { filePath, lineNumber, columnNumber } = splitPathInfo(f.filePath);
return formatWarningOrError({
isError: true,
testName: f.testCase,
filePath,
reason: f.reason,
// cursor,
lineNumber,
columnNumber,
projectRoot: this.props.projectRoot,
maxWarningLineLength: this.props.maxWarningLineLength,
});
}
finish() { }
// Override if you want to catch something specific with your regex
prettyFormat(text) {
return this.parser.parse(text);
}
// If you want to print inline, override #optionalNewline with ''
optionalNewline() {
return '\n';
}
getBuildSummary() {
return `\n\u203A ${this.errors.length} error(s), and ${this.warnings.length} warning(s)\n`;
}
}
exports.Formatter = Formatter;
Formatter.format = format;
Formatter.formatBreadCrumb = formatBreadCrumb;
Formatter.getAppRoot = getAppRoot;
Formatter.highlightLastPathComponent = highlightLastPathComponent;
Formatter.relativePath = relativePath;
function formatPaths(config) {
const filePath = chalk_1.default.reset.cyan(config.filePath);
return (chalk_1.default.dim('(') +
filePath +
chalk_1.default.dim(`:${[config.line, config.col].filter(Boolean).join(':')})`));
}
/**
* Split a string like `/Users/foo/bar.m:420:68` into its components.
*
* @param filePath '/Users/foo/bar.m:420:68'
*/
function splitPathInfo(filePathAndLocation) {
const [path, line, column] = filePathAndLocation.split(':');
return {
filePath: path || filePathAndLocation,
lineNumber: line,
columnNumber: column,
};
}
function parseOptionalInt(text) {
if (!text)
return undefined;
try {
const result = parseInt(text, 10);
return isNaN(result) ? undefined : result;
}
catch {
return undefined;
}
}
function formatWarningOrError({ projectRoot, filePath, reason, cursor, lineText, lineNumber, columnNumber, isError, maxWarningLineLength = 200, }) {
var _a;
const line = parseOptionalInt(lineNumber) || 0;
const column = parseOptionalInt(columnNumber);
const color = isError ? chalk_1.default.red : chalk_1.default.yellow;
const icon = color(isError ? symbols_1.ERROR : symbols_1.WARNING);
const displayFilePath = !filePath
? // If no file path, use null
null
: // If the file path is inside of the build folder (Hermes), then use absolute path.
getAppRoot(filePath)
? filePath
: // Otherwise, use relative path
slash(path.relative(projectRoot, filePath));
const formattedPath = formatPaths({
filePath: displayFilePath,
col: column,
line,
});
const pathWithPrefix = `${icon} ${formattedPath}`;
const formattedReason = color(grayOutMatch(reason, /(\[-.*?\])/).replace(/(\(.*?\)\s?)/, ''));
// Add special case for .jsbundle files that are parsed with Hermes.
const isHermes = filePath.endsWith('.jsbundle');
const isPreviewTooLong = isHermes || (lineText && lineText.length > maxWarningLineLength);
// When the preview is too long, we skip reading the file and attempting to apply
// code coloring, this is because it can get very slow.
if (isPreviewTooLong) {
let previewLine = '';
let cursorLine = formattedReason;
// Create a curtailed preview line like:
// `...transition:'fade'},k._updatePropsStack=function(){clearImmediate(k._updateImmediate),k._updateImmediate...`
// If there is no text preview or column number, we can't do anything.
if (lineText && column != null) {
const rangeWindow = Math.round(Math.max((_a = displayFilePath === null || displayFilePath === void 0 ? void 0 : displayFilePath.length) !== null && _a !== void 0 ? _a : 0, 80) / 2);
let minBounds = Math.max(0, column - rangeWindow);
const maxBounds = Math.min(minBounds + rangeWindow * 2, lineText.length);
previewLine = lineText.slice(minBounds, maxBounds);
// If we splice content off the start, then we should append `...`.
// This is unlikely to happen since we limit the activation size.
if (minBounds > 0) {
// Adjust the min bounds so the cursor is aligned after we add the "..."
minBounds -= 3;
previewLine = chalk_1.default.dim('...') + previewLine;
}
if (maxBounds < lineText.length) {
previewLine += chalk_1.default.dim('...');
}
// If the column property could be found, then use that to fix the cursor location which is often broken in regex.
cursorLine =
(column == null ? chalk_1.default.reset(cursor) : fill(column) + chalk_1.default.reset('^')).slice(minBounds) +
' ' +
formattedReason;
}
return ['', pathWithPrefix, '', previewLine, cursorLine, chalk_1.default.dim('(warning truncated)')].join('\n');
}
try {
const raw = fs.readFileSync(filePath, 'utf8');
const location = { start: { line, column } };
const framed = code_frame_1.codeFrameColumns(raw, location, {
// TODO: Support iOS languages: C++, Objc, swift, Ruby, Bash
// Maybe something like prism but for terminals?
highlightCode: false,
// Remove `(_Nonnull, _Nullable, or _Null_unspecified)` options
message: formattedReason,
});
return `\n${pathWithPrefix}\n\n${framed}\n`;
}
catch {
// If the column property could be found, then use that to fix the cursor location which is often broken in regex.
const customCursor = column == null ? chalk_1.default.reset(cursor) : fill(column) + chalk_1.default.reset('^');
const framed = `${lineText}\n${customCursor} ${formattedReason}`;
return `\n${pathWithPrefix}\n\n${framed}\n`;
}
}
function fill(width) {
return Array(width).join(' ');
}
// Dim values like `[-Wnullability-completeness]`
function grayOutMatch(text, reg) {
return replaceMatch(text, reg, chalk_1.default.gray.dim);
}
function replaceMatch(text, reg, callback) {
const match = text.match(reg);
if (match === null || match === void 0 ? void 0 : match.length) {
return text.replace(reg, callback(match[0]));
}
return text;
}
function slash(path) {
const isExtendedLengthPath = /^\\\\\?\\/.test(path);
const hasNonAscii = /[^\u0000-\u0080]+/.test(path); // eslint-disable-line no-control-regex
if (isExtendedLengthPath || hasNonAscii) {
return path;
}
return path.replace(/\\/g, '/');
}
//# sourceMappingURL=Formatter.js.map