@keymanapp/kmc
Version:
Keyman Developer compiler command line tools
260 lines (255 loc) • 10.5 kB
JavaScript
import * as fs from 'fs';
import * as path from 'path';
import { Option } from 'commander';
import { escapeMarkdownChar, KeymanUrls, CompilerError, dedentCompilerMessageDetail } from '@keymanapp/developer-utils';
import { messageNamespaceKeys, messageSources } from '../messages/messageNamespaces.js';
import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js';
import { exitProcess } from '../util/sysexits.js';
import { InfrastructureMessages } from '../messages/infrastructureMessages.js';
import { findMessageDetails, findMessagesById, getMessageIdentifiersSorted } from '../util/extendedCompilerOptions.js';
;
export function declareMessage(program) {
program
.command('message [messages...]')
.description(`Describe one or more compiler messages. Note: Markdown format is always written to files on disk.`)
.addHelpText('after', `
Message identifiers can be:
* numeric, e.g. "KM07006" or "7006", or
* [namespace.]id, substring id supported; e.g. "kmc-kmn.INFO_MinimumEngineVersion" or "kmc-kmn." or "INFO_Min"`)
.addOption(new Option('-f, --format <format>', 'Output format').choices(['text', 'markdown', 'json']).default('text'))
.option('-o, --out-path <out-path>', 'Output path for Markdown files; output filename for text and json formats')
.option('-a, --all-messages', 'Emit descriptions for all messages (text, json)')
.action(messageCommand);
}
async function messageCommand(messages, _options, commander) {
const commanderOptions = commander.optsWithGlobals();
const options = initialize(commanderOptions);
if (!options) {
await exitProcess(1);
}
const callbacks = new NodeCompilerCallbacks(options);
let result = false;
if (options.format == 'markdown') {
result = messageCommandMarkdown(messages, options, callbacks);
}
else { // json or text format
if (messages.length == 0 && !options.allMessages) {
console.error('Must specify at least one message code or -a for all messages');
callbacks.reportMessage(InfrastructureMessages.Error_MustSpecifyMessageCode());
await exitProcess(1);
}
const messageDetails = messages.length
? messages.flatMap(message => translateMessageInputToCode(message, callbacks))
: allMessageDetails();
if (callbacks.messageCount > 0) {
await exitProcess(1);
}
let text = null;
if (options.format == 'json') {
const data = getMessagesAsArrayForJson(messageDetails);
if (data) {
text = JSON.stringify(data, null, 2);
}
}
else {
text = getMessagesAsText(messageDetails);
}
result = text != null;
if (result) {
if (options.outPath) {
fs.writeFileSync(options.outPath, text, 'utf-8');
}
else {
process.stdout.write(text);
}
}
}
if (!result) {
await exitProcess(1);
}
}
// We have a redirect pattern for kmn.sh/km<#####> to
// the corresponding compiler message reference document in
// help.keyman.com/developer/latest-version/reference/errors/km<#####>
const helpUrl = (code) => KeymanUrls.COMPILER_ERROR_CODE(CompilerError.formatCode(code).toLowerCase());
const getModuleName = (ms) => `${ms.module}.${ms.class.name}`;
function parseMessageIdentifier(message, callbacks) {
const parts = message.split('.', 2);
if (parts.length == 1) {
// searching all namespaces
return { id: parts[0] };
}
// searching one namespace
const namespace = messageNamespaceKeys.find(ns => messageSources[ns].module == parts[0].toLowerCase());
if (!namespace) {
return {};
}
return { namespace, id: parts[1] };
}
function translateMessageInputToCode(message, callbacks) {
const pattern = /^(KM)?([0-9a-f]+)$/i;
const result = message.match(pattern);
if (!result) {
const { namespace, id } = parseMessageIdentifier(message.toLowerCase(), callbacks);
if (!namespace && !id) {
callbacks.reportMessage(InfrastructureMessages.Error_MessageNamespaceNameNotFound({ message }));
return null;
}
// We assume that this is a INFO, HINT, ERROR, etc message, and do a substring search
const items = findMessagesById(namespace, id);
if (!items.length) {
callbacks.reportMessage(InfrastructureMessages.Error_UnrecognizedMessageCode({ message }));
return null;
}
return items;
}
const code = Number.parseInt(result[2], 16);
return [findMessageDetails(code, callbacks)];
}
function initialize(options) {
// We don't want to rename command line options to match the precise
// properties that we have in CompilerOptions, but nor do we want to rename
// CompilerOptions properties...
return {
// CompilerBaseOptions
logLevel: options.logLevel,
logFormat: options.logFormat,
color: options.color,
// MessageOptions
format: options.format ?? 'text',
outPath: options.outPath
};
}
function allMessageDetails() {
let result = [];
messageNamespaceKeys.forEach((namespace) => {
const ms = messageSources[namespace];
const ids = getMessageIdentifiersSorted(ms.class);
for (const id of ids) {
const code = ms.class[id];
if (typeof code != 'number') {
continue;
}
result.push({
code,
id,
class: ms.class,
module: ms.module
});
}
});
return result;
}
const toTitleCase = (s) => s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase();
function getMessageDetail(cls, id, escapeMarkdown) {
const o = /^(DEBUG|VERBOSE|INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(id);
if (!o) {
throw new Error(`Unexpected compiler message ${id}, does not match message error format`);
}
const f = toTitleCase(o[1]) + '_' + o[2];
const event = cls[f]?.({} /* ignore arguments*/);
if (!event) {
throw new Error(`Call to ${cls.name}.${f} returned null`);
}
event.detail = dedentCompilerMessageDetail(event);
event.message = event.message ?? '';
event.message = event?.exceptionVar
? 'This is an internal error; the message will vary'
: (escapeMarkdown ? `${escapeMarkdownChar(event.message, false)}` : event.message);
return event;
}
/*---------------------------------------------------------------------------
* Get messages in text format
*---------------------------------------------------------------------------*/
function getMessagesAsText(messages) {
const result = messages.reduce((prev, message) => {
return prev + '\n' + formatMessageAsText(getModuleName({ class: message.class, module: message.module }), message.class, message.code, message.id);
}, '');
return result;
}
function formatMessageAsText(moduleName, cls, code, id) {
const message = getMessageDetail(cls, id, false);
return `${id}
* Code: ${CompilerError.formatCode(code)}
* Module: ${moduleName}
* Message: ${message.message}
* Reference: ${helpUrl(code)}
${message.detail}
`;
}
/*---------------------------------------------------------------------------
* Get messages as array of objects, for export to JSON
*---------------------------------------------------------------------------*/
function getMessagesAsArrayForJson(messages) {
return messages.map(message => ({
code: CompilerError.formatCode(message.code),
id: message.id,
class: message.class.name,
module: message.module,
detail: (() => getMessageDetail(message.class, message.id, false).detail)()
}));
}
/*---------------------------------------------------------------------------
* Export as Markdown
*---------------------------------------------------------------------------*/
function messageCommandMarkdown(messages, options, callbacks) {
if (messages.length) {
callbacks.reportMessage(InfrastructureMessages.Error_MessagesCannotBeFilteredForMarkdownFormat());
return false;
}
if (!options.outPath) {
callbacks.reportMessage(InfrastructureMessages.Error_OutputPathMustBeSpecifiedForMarkdownFormat());
return false;
}
if (!fs.existsSync(options.outPath) || !fs.statSync(options.outPath)?.isDirectory()) {
callbacks.reportMessage(InfrastructureMessages.Error_OutputPathMustExistAndBeADirectory({ outPath: options.outPath }));
return false;
}
exportAllMessagesAsMarkdown(options.outPath);
return true;
}
function exportAllMessagesAsMarkdown(outPath) {
let index = `---
title: Compiler Messages Reference
---
`;
messageNamespaceKeys.forEach((namespace) => {
const ms = messageSources[namespace];
const moduleName = getModuleName(ms);
index += `* [${moduleName}](${moduleName.toLowerCase()})\n`;
exportModuleMessagesAsMarkdown(moduleName, ms, outPath);
});
fs.writeFileSync(path.join(outPath, 'index.md'), index, 'utf-8');
}
function exportModuleMessagesAsMarkdown(moduleName, ms, outPath) {
const cls = ms.class;
let index = `---
title: Compiler Messages Reference for @keymanapp/${ms.module}
---
Code | Identifier | Message
------|------------|---------
`;
const ids = getMessageIdentifiersSorted(cls);
for (const id of ids) {
const code = cls[id];
const filename = CompilerError.formatCode(code).toLowerCase();
const message = getMessageDetail(cls, id, true);
const content = formatMessageAsMarkdown(moduleName, id, message);
index += `[${CompilerError.formatCode(code)}](${filename}) | \`${id}\` | ${message.message}\n`;
fs.writeFileSync(path.join(outPath, filename + '.md'), content, 'utf-8');
}
fs.writeFileSync(path.join(outPath, moduleName.toLowerCase() + '.md'), index, 'utf-8');
}
function formatMessageAsMarkdown(moduleName, id, message) {
return `---
title: ${CompilerError.formatCode(message.code)}: ${id}
---
| | |
|------------|---------- |
| Message | ${message.message} |
| Module | [${moduleName}](${moduleName.toLowerCase()}) |
| Identifier | \`${id}\` |
${message.detail}
`;
}
//# sourceMappingURL=messageCommand.js.map