UNPKG

@softkit/i18n

Version:

This library is a simple wrapper based on [nestjs-i18n](https://nestjs-i18n.com/)

388 lines 15.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GenerateTypesCommand = void 0; const tslib_1 = require("tslib"); const loaders_1 = require("../loaders"); const chalk_1 = tslib_1.__importDefault(require("chalk")); const utils_1 = require("../utils"); const node_fs_1 = tslib_1.__importDefault(require("node:fs")); const node_path_1 = tslib_1.__importDefault(require("node:path")); const node_process_1 = tslib_1.__importDefault(require("node:process")); const chokidar_1 = tslib_1.__importDefault(require("chokidar")); const fs_extra_1 = require("fs-extra"); const import_1 = require("../utils/import"); const typescript_1 = require("../utils/typescript"); class GenerateTypesCommand { constructor() { this.command = 'generate-types'; this.describe = 'Generate types for translations. Supports json and yaml files.'; } builder(args) { return args .option('debounce', { alias: 'd', type: 'number', describe: 'Debounce time in ms', default: 200, demandOption: false, }) .option('optionsFile', { alias: 'opt', type: 'string', describe: 'Options file path', demandOption: false, }) .option('watch', { alias: 'w', type: 'boolean', describe: 'Watch for changes and generate types', default: false, demandOption: false, }) .option('typesOutputPath', { alias: 'o', type: 'string', describe: 'Path to output types file', default: 'src/generated/i18n.generated.ts', demandOption: false, }) .option('loaderType', { alias: 't', type: 'string', array: true, options: ['json', 'yaml'], describe: 'Loader type', demandOption: false, default: [], }) .option('translationsPath', { alias: 'p', type: 'string', describe: 'Path to translations', array: true, default: [], demandOption: false, }); } async handler(args) { const { packageConfig = {}, packageJsonFilePath } = (await getPackageConfig()) || {}; packageConfig['i18n'] = packageConfig['i18n'] ?? {}; if (!args.typesOutputPath && packageConfig['i18n'].typesOutputPath) { args.typesOutputPath = packageConfig['i18n'].typesOutputPath; } if (args.optionsFile) { args.optionsFile = node_path_1.default.resolve(node_process_1.default.cwd(), args.optionsFile); } if (!args.optionsFile && packageConfig['i18n'].optionsFile) { const packageJsonFolder = node_path_1.default.dirname(packageJsonFilePath); args.optionsFile = node_path_1.default.join(packageJsonFolder, packageConfig['i18n'].optionsFile); } if (!args.typesOutputPath) { console.log(chalk_1.default.red(`Error: typesOutputPath is not defined. Please provide a path to output types file, in params or in package.json`)); node_process_1.default.exit(1); } args.translationsPath = sanitizePaths(args.translationsPath); validateInputParams(args); validatePathsNotEmbeddedInEachOther(args.translationsPath); const optionsFromFile = await validateAndGetOptionsFile(args.optionsFile); const loaders = args.loaderType.map((loaderType, index) => { const path = args.translationsPath[index]; validatePath(path, loaderType, index); return { path, loader: getLoaderByType(loaderType, path), }; }); for (const loader of optionsFromFile?.loaders || []) { const p = loader?.options?.path; loaders.push({ path: p ?? sanitizePath(p), loader: loader, }); } const translationsWithPaths = await loadTranslations(loaders); const validTranslationsWithPaths = translationsWithPaths.filter((item) => Boolean(item.path)); let hasError = false; const translationsMapped = translationsWithPaths.map(({ translations, error, path }) => { if (error) { console.log(chalk_1.default.red(`Error while loading translations from ${path}: ${error.message}`)); hasError = true; } return translations; }); const validTranslations = translationsMapped.filter((translation) => translation !== null && translation !== undefined); if (!hasError && validTranslations.length > 0) { const mergedTranslations = reduceTranslations(validTranslations); await generateAndSaveTypes(mergedTranslations, args); } else if (!args.watch) { node_process_1.default.exit(1); } if (args.watch) { console.log(chalk_1.default.green(`Listening for changes in ${args.translationsPath.join(', ')}...`)); if (this.fsWatcher === undefined) { this.fsWatcher = await listenForChanges(loaders, validTranslationsWithPaths, args); } } else { node_process_1.default.exit(0); } } async stopWatcher() { if (this.fsWatcher) { await this.fsWatcher.close(); } } } exports.GenerateTypesCommand = GenerateTypesCommand; /** * we do not support nested paths, because listeners will be triggered multiple times * and it doesn't really make sense to have the same folder twice * */ function validatePathsNotEmbeddedInEachOther(paths) { for (let i = 0; i < paths.length; i++) { const pathToCheck = paths[i]; for (const [j, pathToCompare] of paths.entries()) { if (j !== i && pathToCheck.startsWith(pathToCompare)) { console.log(chalk_1.default.red(`Path ${pathToCheck} is embedded in ${pathToCompare}. This is not supported.`)); node_process_1.default.exit(1); } } } } function listenForChanges(loadersWithPaths, translationsWithPaths, args) { const allPaths = loadersWithPaths.map(({ path }) => path); const loadersByPath = loadersWithPaths.reduce((acc, { path, loader }) => { acc[path] = loader; return acc; }, {}); return new Promise((resolve, reject) => { const fsWatcher = chokidar_1.default .watch(allPaths, { ignoreInitial: true, }) .on('ready', () => { resolve(fsWatcher); }) .on('error', (error) => { console.log(chalk_1.default.red(`Error while watching files: ${error.message}`)); reject(error); }) .on('all', customDebounce(handleFileChangeEvents(allPaths, loadersByPath, translationsWithPaths, args), args.debounce)); }); } function sanitizePath(path) { // adding trailing slash const newPath = path.endsWith('/') ? path : `${path}/`; // removing starting slash return newPath.startsWith('./') ? newPath.slice(2) : newPath; } function sanitizePaths(paths) { return paths.map((path) => { return sanitizePath(path); }); } function handleFileChangeEvents(listenToPaths, loadersByPath, translationsWithPaths, args) { return async (events, paths) => { console.log(chalk_1.default.blue(`Change detected`)); console.log(chalk_1.default.green( // eslint-disable-next-line sonarjs/no-nested-template-literals `${events.map((e, idx) => `\t${e} - ${paths[idx]}`).join('\n')}`)); console.log(chalk_1.default.blue(`Re-generating types...`)); const uniquePaths = new Set(); for (const changePath of paths) { const foundPath = listenToPaths.find((path) => changePath.startsWith(path)); if (foundPath) { uniquePaths.add(foundPath); } if (uniquePaths.size === paths.length) { break; } } let hasError = false; for (const path of uniquePaths) { const loader = loadersByPath[path]; try { const translation = (await loader.load()); for (const translationWithPath of translationsWithPaths) { if (translationWithPath.path === path) { translationWithPath.translations = translation; } } } catch (error) { hasError = true; if (error instanceof Error) { console.log(chalk_1.default.red(`Error while loading translations from ${path}. Error: ${error.message}`)); } else { console.log(chalk_1.default.red(`Error while loading translations from ${path}. Error: ${JSON.stringify(error)}`)); } } } if (hasError) { console.log(chalk_1.default.red(`Waiting for changes to generate proper types`)); return; } const mergedTranslations = reduceTranslations(translationsWithPaths.map(({ translations }) => translations)); await generateAndSaveTypes(mergedTranslations, args); }; } async function generateAndSaveTypes(translations, args) { const object = Object.keys(translations).reduce((result, key) => (0, utils_1.mergeDeep)(result, translations[key]), {}); const rawContent = await (0, typescript_1.createTypesFile)(object); const outputFile = (0, typescript_1.annotateSourceCode)(rawContent); node_fs_1.default.mkdirSync(node_path_1.default.dirname(args.typesOutputPath), { recursive: true, }); let currentFileContent = null; try { currentFileContent = node_fs_1.default.readFileSync(args.typesOutputPath, 'utf8'); } catch { // expected empty line // eslint-disable-next-line no-empty } if (currentFileContent == outputFile) { console.log(` ${chalk_1.default.yellow('No changes generated in a result output type file.')} `); } else { node_fs_1.default.writeFileSync(args.typesOutputPath, outputFile); console.log(` ${chalk_1.default.green(`Types generated and saved to: ${args.typesOutputPath}`)} `); } } function customDebounce(func, wait) { let args = []; let timeoutId; return function (...rest) { // User formal parameters to make sure we add a slot even if a param // is not passed in if (func.length > 0) { for (let i = 0; i < func.length; i++) { if (!args[i]) { args[i] = []; } args[i].push(rest[i]); } } // No formal parameters, just track the whole argument list else { args.push(...rest); } clearTimeout(timeoutId); timeoutId = setTimeout(function () { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore func.apply(this, args); args = []; }, wait); }; } async function loadTranslations(loaders) { const loadedTranslations = await Promise.all(loaders.map(({ loader }) => loader.load().catch((error) => error))); return loadedTranslations.map((result, index) => { const isError = result instanceof Error; return { translations: isError ? null : result, error: isError ? result : null, path: loaders[index].path, }; }); } async function validateAndGetOptionsFile(optionsFile) { if (optionsFile) { let optionsFileExport; try { optionsFileExport = await (0, import_1.importOrRequireFile)(optionsFile); } catch (error) { throw error instanceof Error ? new Error(`Unable to open file: "${optionsFile}". ${error.message}`) : new Error(`Unable to open file: "${optionsFile}". `); } if (!optionsFileExport || typeof optionsFileExport !== 'object') { throw new Error(`Given options file must contain export of a I18nOptions instance`); } const optionsExported = []; for (const key in optionsFileExport) { const options = optionsFileExport[key]; if (options.loaders) { optionsExported.push(options); } } if (optionsExported.length === 0) { throw new Error(`Given options file must contain export of a I18nOptions`); } if (optionsExported.length > 1) { throw new Error(`Given options file must contain only one export of I18nOptions`); } return optionsExported[0]; } } function validateInputParams(args) { if (args.loaderType.length !== args.translationsPath.length) { console.log(chalk_1.default.red(`Error: translationsPath and loaderType must have the same number of elements. You provided ${args.loaderType.length} loader types and ${args.translationsPath.length} paths`)); node_process_1.default.exit(1); } if ((args.loaderType.length === 0 || args.loaderType.length === 0) && !args.optionsFile) { console.log(chalk_1.default.red(`Error: you must provide at least one loader type or options file`)); node_process_1.default.exit(1); } } function validatePath(path, loaderType, index) { if (path === undefined) { console.log(chalk_1.default.red(`Error: translationsPath is not defined for loader type ${loaderType}, please provide a path to translations, index ${index}`)); node_process_1.default.exit(1); } } async function getPackageConfig(basePath = node_process_1.default.cwd()) { const packageJsonFilePath = `${basePath}/package.json`; if (await (0, fs_extra_1.pathExists)(packageJsonFilePath)) { /* istanbul ignore next */ try { const packageConfig = await require(packageJsonFilePath); return { packageJsonFilePath, packageConfig, }; } catch { throw new Error(`Failed to load package.json`); } } const parentFolder = await (0, fs_extra_1.realpath)(`${basePath}/..`); // we reached the root folder if (basePath === parentFolder) { throw new Error(`Reached the root folder without finding package.json in ${basePath}`); } return getPackageConfig(parentFolder); } function getLoaderByType(loaderType, path) { switch (loaderType) { case 'json': { return new loaders_1.I18nJsonLoader({ path, }); } case 'yaml': { return new loaders_1.I18nYamlLoader({ path, }); } default: { console.log(chalk_1.default.red(`Error: loader type ${loaderType} is not supported`)); node_process_1.default.exit(1); } } } function reduceTranslations(translations) { return translations.reduce((acc, t) => (0, utils_1.mergeTranslations)(acc, t), {}); } //# sourceMappingURL=generate-types.command.js.map