UNPKG

@keymanapp/kmc

Version:

Keyman Developer compiler command line tools

292 lines 11.8 kB
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