@ima/cli
Version:
IMA.js CLI tool to build, develop and work with IMA.js applications.
217 lines (212 loc) • 9.26 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getLanguageModulePath = getLanguageModulePath;
exports.getLanguageEntryPath = getLanguageEntryPath;
exports.getDictionaryKeyFromFileName = getDictionaryKeyFromFileName;
exports.getLanguageEntryPoints = getLanguageEntryPoints;
exports.generateTypeDeclarations = generateTypeDeclarations;
exports.parseLanguageFiles = parseLanguageFiles;
exports.compileLanguages = compileLanguages;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const logger_1 = require("@ima/dev-utils/logger");
const helpers_1 = require("@ima/helpers");
const core_1 = __importDefault(require("@messageformat/core"));
const compile_module_1 = __importDefault(require("@messageformat/core/lib/compile-module"));
const chalk_1 = __importDefault(require("chalk"));
const chokidar_1 = __importDefault(require("chokidar"));
const globby_1 = __importDefault(require("globby"));
const TMP_BASEPATH = './build/tmp';
/**
* Returns path to location of compiled messageformat JS modules
* for given locale.
*
* @param locale Currently processed locale identifier.
* @param rootDir Current compilation root directory.
* @returns Path to compiled locale module.
*/
function getLanguageModulePath(locale, rootDir) {
return path_1.default.join(rootDir, TMP_BASEPATH, `/locale/${locale}.module.js`);
}
/**
* Returns path to location of compiled messageformat JS modules
* for given locale.
*
* @param locale Currently processed locale identifier.
* @param rootDir Current compilation root directory.
* @returns Path to compiled locale module.
*/
function getLanguageEntryPath(locale, rootDir) {
return path_1.default.join(rootDir, TMP_BASEPATH, `/locale/${locale}.js`);
}
/**
* Parses dictionary key from given filename and locale identifier.
*
* @param locale Currently processed locale identifier.
* @param languagePath Path to currently processed JSON language file.
* @returns Parsed dictionary key.
*/
function getDictionaryKeyFromFileName(locale, languagePath) {
return path_1.default.parse(languagePath).name.replace(locale.toUpperCase(), '');
}
/**
* Returns entry points to use in webpack configurations. These then lead to
* messageformat compiled modules while also containing some additional runtime code.
*
* @param languages Languages object from ima config.
* @param rootDir Current compilation root directory.
* @returns Object with webpack entry points.
*/
function getLanguageEntryPoints(languages, rootDir, useHMR = false) {
return Object.keys(languages).reduce((resultEntries, locale) => {
const entryPath = getLanguageEntryPath(locale, rootDir);
const modulePath = getLanguageModulePath(locale, rootDir);
let content = `
import message from './${path_1.default.basename(modulePath)}';
(function () {var $IMA = {}; if ((typeof window !== "undefined") && (window !== null)) { window.$IMA = window.$IMA || {}; $IMA = window.$IMA; }
$IMA.i18n = message;
})();
export default message;
`;
if (useHMR) {
content += `
if (module.hot) {
module.hot.accept('./${path_1.default.basename(modulePath)}', () => {
$IMA.i18n = message;
window.__IMA_HMR.emitter.emit('update', { type: 'languages' })
});
}
`;
}
if (!fs_1.default.existsSync(entryPath)) {
fs_1.default.mkdirSync(path_1.default.dirname(entryPath), { recursive: true });
}
fs_1.default.writeFileSync(entryPath, content);
return Object.assign(resultEntries, {
[`locale/${locale}`]: entryPath,
});
}, {});
}
async function generateTypeDeclarations(rootDir, messages) {
const dictionaryMap = new Map();
const dictionaryTypesPath = path_1.default.join(rootDir, TMP_BASEPATH, '/types/dictionary.ts');
(function recurseMessages(messages, path = '') {
if (typeof messages === 'object') {
Object.keys(messages).forEach(key => {
recurseMessages(messages[key], `${path ? path + '.' : path}${key}`);
});
}
else {
dictionaryMap.set(path, messages);
}
})(messages);
const content = `declare module '@ima/core' {
interface DictionaryMap {
${Array.from(dictionaryMap.keys())
.map(key => `'${key}': string;`)
.join('\n\t\t')}
}
}
export { };
`;
if (!fs_1.default.existsSync(path_1.default.dirname(dictionaryTypesPath))) {
await fs_1.default.promises.mkdir(path_1.default.dirname(dictionaryTypesPath));
}
await fs_1.default.promises.writeFile(path_1.default.join(rootDir, TMP_BASEPATH, '/types/dictionary.ts'), content);
}
/**
* Parses language JSON files at languagePaths into messages dictionary object,
* compiles the final messages object into messageformat JS module and outputs
* it to filesystem at outputPath.
*
* @param messages Object which contains dictionary of parsed languages.
* @param locale Currently processed locale identifier.
* @param languagePaths Paths to JSON language files which should be processed.
* @param outputPath Output path for the messageformat JS module.
*/
async function parseLanguageFiles(messages, locale, languagePaths, outputPath) {
// Load language JSON files and parse them into messages dictionary
await Promise.all((Array.isArray(languagePaths) ? languagePaths : [languagePaths]).map(async (languagePath) => {
try {
const dictionaryKey = getDictionaryKeyFromFileName(locale, languagePath);
messages[dictionaryKey] = (0, helpers_1.assignRecursively)(messages[dictionaryKey] ?? {}, JSON.parse((await fs_1.default.promises.readFile(languagePath)).toString()));
}
catch (error) {
throw new Error(`Unable to parse language file at location: ${chalk_1.default.magenta(languagePath)}\n\n${error?.message}`);
}
}));
// Write changes to language JS module
const compiledModule = (0, compile_module_1.default)(new core_1.default(locale), messages);
await fs_1.default.promises.writeFile(outputPath, compiledModule);
}
/**
* Compile language files defined in imaConfig.
*
* @param imaConfig ima.config.js file contents.
* @param rootDir Current compilation root directory.
* @param watch When set to true, it creates chokidar instances
* which watch language files for changes and trigger recompilation.
*/
async function compileLanguages(imaConfig, rootDir, watch = false) {
const locales = Object.keys(imaConfig.languages);
const modulesBaseDir = path_1.default.dirname(getLanguageModulePath('en', rootDir));
if (!fs_1.default.existsSync(modulesBaseDir)) {
await fs_1.default.promises.mkdir(modulesBaseDir, { recursive: true });
}
await Promise.all(locales.map(async (locale, index) => {
const messages = {};
const outputPath = getLanguageModulePath(locale, rootDir);
for (const glob of imaConfig.languages[locale]) {
const languagePaths = await (0, globby_1.default)(glob, {
cwd: rootDir,
absolute: true,
});
// Parse the language files
await parseLanguageFiles(messages, locale, languagePaths, outputPath);
}
// Run only for first language file to avoid conflicts
if (index === 0) {
// Don't await since it can be compiled lazily
generateTypeDeclarations(rootDir, messages);
}
if (!watch) {
return;
}
// Create chokidar instance for every language in watch mode
chokidar_1.default
.watch(imaConfig.languages[locale], {
ignoreInitial: true,
cwd: rootDir,
})
.on('all', async (eventName, changedRelativePath) => {
if (!['unlink', 'add', 'change'].includes(eventName)) {
return;
}
try {
const changedLanguagePath = path_1.default.join(rootDir, changedRelativePath);
/**
* Remove deleted langauge file dictionary keys from messages.
*/
if (eventName === 'unlink') {
delete messages[getDictionaryKeyFromFileName(locale, changedLanguagePath)];
}
// Don't reload any file when it is deleted
await parseLanguageFiles(messages, locale, eventName === 'unlink' ? [] : [changedLanguagePath], outputPath);
// Run only for first language file to avoid conflicts
if (index === 0) {
// Don't await since it can be compiled lazily
generateTypeDeclarations(rootDir, messages);
}
}
catch (error) {
logger_1.logger.error(error);
}
})
.on('error', error => {
logger_1.logger.error(new Error(`Unexpected error occurred while watching language files\n\n${error.message}`));
});
}));
}