@keymanapp/kmc
Version:
Keyman Developer compiler command line tools
292 lines • 11.8 kB
JavaScript
import * as fs from 'fs';
import * as path from 'path';
import { platform } from 'os';
import { compilerLogLevelToSeverity, CompilerErrorSeverity, CompilerError, CompilerFileCallbacks } from '@keymanapp/developer-utils';
import { InfrastructureMessages } from '../messages/infrastructureMessages.js';
import chalk from 'chalk';
import supportsColor from 'supports-color';
import { KeymanSentry } from '@keymanapp/developer-utils';
import { fileURLToPath } from 'url';
const color = chalk.default;
const severityColors = {
[CompilerErrorSeverity.Info]: color.reset,
[CompilerErrorSeverity.Hint]: color.blueBright,
[CompilerErrorSeverity.Warn]: color.hex('FFA500'), // orange
[CompilerErrorSeverity.Error]: color.redBright,
[CompilerErrorSeverity.Fatal]: color.redBright,
[CompilerErrorSeverity.Verbose]: color.gray,
[CompilerErrorSeverity.Debug]: color.blueBright,
};
/**
* Maximum messages that will be emitted before suppressing further messages.
* We may in the future make this user configurable?
*/
const MaxMessagesDefault = 100;
/**
* Concrete implementation for CLI use
*/
export class NodeCompilerCallbacks {
options;
/* NodeCompilerCallbacks */
_net = new NodeCompilerNetAsyncCallbacks();
_fsAsync = new NodeCompilerFileSystemAsyncCallbacks();
messages = [];
messageCount = 0;
messageFilename = '';
maxLogMessages = MaxMessagesDefault;
constructor(options) {
this.options = options;
color.enabled = this.options.color ?? (supportsColor.stdout ? supportsColor.stdout.hasBasic : false);
}
clear() {
this.messages = [];
this.messageCount = 0;
this.messageFilename = '';
}
/**
* Returns true if any message in the log is a Fatal, Error, or if we are
* treating warnings as errors, a Warning. The warning option will be taken
* from the CompilerOptions passed to the constructor, or the parameter, to
* allow for per-file overrides (as seen with projects, for example).
* @param compilerWarningsAsErrors
* @returns
*/
hasFailureMessage(compilerWarningsAsErrors) {
return CompilerFileCallbacks.hasFailureMessage(this.messages,
// parameter overrides global option
compilerWarningsAsErrors ?? this.options.compilerWarningsAsErrors);
}
hasMessage(code) {
return this.messages.find((item) => item.code == code) === undefined ? false : true;
}
verifyFilenameConsistency(originalFilename) {
if (fs.existsSync(originalFilename)) {
// Note, we only check this if the file exists, because
// if it is not found, that will be returned as an error
// from loadFile anyway.
let filename = fs.realpathSync(originalFilename);
let nativeFilename = fs.realpathSync.native(filename);
if (platform() == 'win32' && originalFilename.match(/^.:/)) {
// When an absolute path is passed in, it includes a drive letter.
// Drive letter case can differ but we don't care about that on win32.
// Typically absolute paths only appear for input parameters, as absolute
// paths are flagged as warnings when they appear in source files anyway.
// Upper casing the drive letter just avoids the issue.
filename = filename[0].toUpperCase() + filename.substring(1);
nativeFilename = nativeFilename[0].toUpperCase() + nativeFilename.substring(1);
}
if (filename != nativeFilename) {
this.reportMessage(InfrastructureMessages.Hint_FilenameHasDifferingCase({
reference: path.basename(originalFilename),
filename: path.basename(nativeFilename)
}));
}
}
}
/* CompilerCallbacks */
loadFile(filename) {
this.verifyFilenameConsistency(filename);
try {
return fs.readFileSync(filename);
}
catch (e) {
if (e.code === 'ENOENT') {
return null;
}
else {
throw e;
}
}
}
fileSize(filename) {
return fs.statSync(filename)?.size;
}
isDirectory(filename) {
return fs.statSync(filename)?.isDirectory();
}
get path() {
return path;
}
get fs() {
return fs;
}
get net() {
return this._net;
}
get fsAsync() {
return this._fsAsync;
}
fileURLToPath(url) {
return fileURLToPath(url);
}
reportMessage(event) {
if (!event.filename) {
event.filename = this.messageFilename;
}
if (this.messageFilename != event.filename) {
// Reset max message limit when a new file is being processed
this.messageFilename = event.filename;
this.messageCount = 0;
}
const disable = CompilerFileCallbacks.applyMessageOverridesToEvent(event, this.options.messageOverrides);
this.messages.push({ ...event });
// report fatal errors to Sentry, but don't abort; note, it won't be
// reported if user has disabled the Sentry setting
if (CompilerError.severity(event.code) == CompilerErrorSeverity.Fatal) {
// this is async so returns a Promise, we'll let it resolve in its own
// time, and it will emit a message to stderr with details at that time
KeymanSentry.reportException(event.exceptionVar ?? event.message, false);
}
if (disable || CompilerError.severity(event.code) < compilerLogLevelToSeverity[this.options.logLevel]) {
// collect messages but don't print to console
return;
}
// We don't use this.messages.length because we only want to count visible
// messages, and there's no point in recalculating the total for every
// message emitted.
this.messageCount++;
if (this.messageCount > this.maxLogMessages) {
return;
}
if (this.messageCount == this.maxLogMessages) {
// We've hit our event limit so we'll suppress further messages, and emit
// our little informational message so users know what's going on. Note
// that this message will not be included in the this.messages array, and
// that will continue to collect all messages; this only affects the
// console emission of messages.
event = InfrastructureMessages.Info_TooManyMessages({ count: this.maxLogMessages });
event.filename = this.messageFilename;
}
this.printMessage(event);
}
printMessage(event) {
if (this.options.logFormat == 'tsv') {
this.printTsvMessage(event);
}
else {
this.printFormattedMessage(event);
}
}
printTsvMessage(event) {
process.stdout.write([
CompilerError.formatFilename(event.filename, { fullPath: true, forwardSlashes: false }),
CompilerError.formatLine(event.line),
CompilerError.formatSeverity(event.code),
CompilerError.formatCode(event.code),
CompilerError.formatMessage(event.message)
].join('\t') + '\n');
}
printFormattedMessage(event) {
const severityColor = severityColors[CompilerError.severity(event.code)] ?? color.reset;
const messageColor = this.messageSpecialColor(event) ?? color.reset;
process.stdout.write((event.filename
? color.cyan(CompilerError.formatFilename(event.filename)) +
(event.line ? ':' + color.yellowBright(CompilerError.formatLine(event.line)) : '') + ' - '
: '') +
severityColor(CompilerError.formatSeverity(event.code)) + ' ' +
color.grey(CompilerError.formatCode(event.code)) + ': ' +
messageColor(CompilerError.formatMessage(event.message)) + '\n');
if (event.code == InfrastructureMessages.INFO_ProjectBuiltSuccessfully) {
// Special case: we'll add a blank line after project builds
process.stdout.write('\n');
}
}
/**
* We treat a few certain infrastructure messages with special colours
* @param event
* @returns
*/
messageSpecialColor(event) {
switch (event.code) {
case InfrastructureMessages.INFO_BuildingFile:
case InfrastructureMessages.INFO_CopyingProject:
case InfrastructureMessages.INFO_GeneratingProject:
return color.whiteBright;
case InfrastructureMessages.INFO_FileNotBuiltSuccessfully:
case InfrastructureMessages.INFO_ProjectNotBuiltSuccessfully:
case InfrastructureMessages.INFO_ProjectNotCopiedSuccessfully:
case InfrastructureMessages.INFO_ProjectNotGeneratedSuccessfully:
return color.red;
case InfrastructureMessages.INFO_FileBuiltSuccessfully:
case InfrastructureMessages.INFO_ProjectBuiltSuccessfully:
case InfrastructureMessages.INFO_ProjectCopiedSuccessfully:
case InfrastructureMessages.INFO_ProjectGeneratedSuccessfully:
return color.green;
}
return null;
}
debug(msg) {
if (this.options.logLevel == 'debug') {
console.debug(msg);
}
}
fileExists(filename) {
return fs.existsSync(filename);
}
resolveFilename(baseFilename, filename) {
return _resolveFilename(baseFilename, filename);
}
}
class NodeCompilerNetAsyncCallbacks {
async fetchBlob(url, options) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
}
const data = await response.blob();
return new Uint8Array(await data.arrayBuffer());
}
catch (e) {
throw new Error(`Error downloading ${url}`, { cause: e });
}
}
async fetchJSON(url, options) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
}
return await response.json();
}
catch (e) {
throw new Error(`Error downloading ${url}`, { cause: e });
}
}
}
class NodeCompilerFileSystemAsyncCallbacks {
async exists(filename) {
return fs.existsSync(filename);
}
async readFile(filename) {
return fs.readFileSync(filename);
}
async readdir(filename) {
return fs.readdirSync(filename).map(item => ({
filename: item,
type: fs.statSync(path.join(filename, item))?.isDirectory() ? 'dir' : 'file'
}));
}
resolveFilename(baseFilename, filename) {
return _resolveFilename(baseFilename, filename);
}
}
function _resolveFilename(baseFilename, filename) {
const basePath = baseFilename.endsWith('/') || baseFilename.endsWith('\\') ?
baseFilename :
path.dirname(baseFilename);
// Transform separators to platform separators -- we are agnostic
// in our use here but path prefers files may use
// either / or \, although older kps files were always \.
if (path.sep == '/') {
filename = filename.replace(/\\/g, '/');
}
else {
filename = filename.replace(/\//g, '\\');
}
if (!path.isAbsolute(filename)) {
filename = path.resolve(basePath, filename);
}
return filename;
}
//# sourceMappingURL=NodeCompilerCallbacks.js.map