@formatjs/cli-lib
Version:
Lib for CLI for formatjs.
193 lines (192 loc) • 7.09 kB
JavaScript
import { interpolateName, } from '@formatjs/ts-transformer';
import { outputFile, readFile } from 'fs-extra';
import { debug, getStdinAsString, warn, writeStdout } from './console_utils';
import { parse } from '@formatjs/icu-messageformat-parser';
import { hoistSelectors } from '@formatjs/icu-messageformat-parser/manipulator';
import { printAST } from '@formatjs/icu-messageformat-parser/printer';
import stringify from 'json-stable-stringify';
import { resolveBuiltinFormatter } from './formatters';
import { parseScript } from './parse_script';
function calculateLineColFromOffset(text, start) {
if (!start) {
return { line: 1, col: 1 };
}
const chunk = text.slice(0, start);
const lines = chunk.split('\n');
const lastLine = lines[lines.length - 1];
return { line: lines.length, col: lastLine.length };
}
async function processFile(source, fn, { idInterpolationPattern, ...opts }) {
let messages = [];
let meta;
const onMsgExtracted = opts.onMsgExtracted;
const onMetaExtracted = opts.onMetaExtracted;
opts = {
...opts,
additionalComponentNames: [
'$formatMessage',
...(opts.additionalComponentNames || []),
],
onMsgExtracted(filePath, msgs) {
if (opts.extractSourceLocation) {
msgs = msgs.map(msg => ({
...msg,
...calculateLineColFromOffset(source, msg.start),
}));
}
messages = messages.concat(msgs);
if (onMsgExtracted) {
onMsgExtracted(filePath, msgs);
}
},
onMetaExtracted(filePath, m) {
meta = m;
if (onMetaExtracted) {
onMetaExtracted(filePath, m);
}
},
};
if (!opts.overrideIdFn && idInterpolationPattern) {
opts = {
...opts,
overrideIdFn: (id, defaultMessage, description, fileName) => id ||
interpolateName({
resourcePath: fileName,
}, idInterpolationPattern, {
content: description
? `${defaultMessage}#${typeof description === 'string'
? description
: stringify(description)}`
: defaultMessage,
}),
};
}
debug('Processing opts for %s: %s', fn, opts);
const scriptParseFn = parseScript(opts, fn);
if (fn.endsWith('.vue')) {
debug('Processing %s using vue extractor', fn);
const { parseFile } = await import('./vue_extractor.js');
parseFile(source, fn, scriptParseFn);
}
else if (fn.endsWith('.hbs')) {
debug('Processing %s using hbs extractor', fn);
const { parseFile } = await import('./hbs_extractor.js');
parseFile(source, fn, opts);
}
else if (fn.endsWith('.gts') || fn.endsWith('.gjs')) {
debug('Processing %s as gts/gjs file', fn);
const { parseFile } = await import('./gts_extractor.js');
parseFile(source, fn, opts);
}
else {
debug('Processing %s using typescript extractor', fn);
scriptParseFn(source);
}
debug('Done extracting %s messages: %s', fn, messages);
if (meta) {
debug('Extracted meta:', meta);
messages.forEach(m => (m.meta = meta));
}
return { messages, meta };
}
/**
* Extract strings from source files
* @param files list of files
* @param extractOpts extract options
* @returns messages serialized as JSON string since key order
* matters for some `format`
*/
export async function extract(files, extractOpts) {
const { throws, readFromStdin, flatten, ...opts } = extractOpts;
let rawResults = [];
try {
if (readFromStdin) {
debug(`Reading input from stdin`);
// Read from stdin
if (process.stdin.isTTY) {
warn('Reading source file from TTY.');
}
const stdinSource = await getStdinAsString();
rawResults = [await processFile(stdinSource, 'dummy', opts)];
}
else {
rawResults = await Promise.all(files.map(async (fn) => {
debug('Extracting file:', fn);
const source = await readFile(fn, 'utf8');
return processFile(source, fn, opts);
}));
}
}
catch (e) {
if (throws) {
throw e;
}
else {
warn(String(e));
}
}
const formatter = await resolveBuiltinFormatter(opts.format);
const extractionResults = rawResults.filter((r) => !!r);
const extractedMessages = new Map();
for (const { messages } of extractionResults) {
for (const message of messages) {
const { id, description, defaultMessage } = message;
if (!id) {
const error = new Error(`[FormatJS CLI] Missing message id for message:
${JSON.stringify(message, undefined, 2)}`);
if (throws) {
throw error;
}
else {
warn(error.message);
}
continue;
}
if (extractedMessages.has(id)) {
const existing = extractedMessages.get(id);
if (stringify(description) !== stringify(existing.description) ||
defaultMessage !== existing.defaultMessage) {
const error = new Error(`[FormatJS CLI] Duplicate message id: "${id}", ` +
'but the `description` and/or `defaultMessage` are different.');
if (throws) {
throw error;
}
else {
warn(error.message);
}
}
}
extractedMessages.set(id, message);
}
}
const results = {};
const messages = Array.from(extractedMessages.values());
for (const { id, ...msg } of messages) {
if (flatten && msg.defaultMessage) {
msg.defaultMessage = printAST(hoistSelectors(parse(msg.defaultMessage)));
}
results[id] = msg;
}
if (typeof formatter.serialize === 'function') {
return formatter.serialize(formatter.format(results));
}
return stringify(formatter.format(results), {
space: 2,
cmp: formatter.compareMessages || undefined,
});
}
/**
* Extract strings from source files, also writes to a file.
* @param files list of files
* @param extractOpts extract options
* @returns A Promise that resolves if output file was written successfully
*/
export default async function extractAndWrite(files, extractOpts) {
const { outFile, ...opts } = extractOpts;
const serializedResult = (await extract(files, opts)) + '\n';
if (outFile) {
debug('Writing output file:', outFile);
return outputFile(outFile, serializedResult);
}
await writeStdout(serializedResult);
}