UNPKG

@ima/cli

Version:

IMA.js CLI tool to build, develop and work with IMA.js applications.

217 lines (212 loc) 9.26 kB
"use strict"; 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}`)); }); })); }