UNPKG

@intlayer/chokidar

Version:

Uses chokidar to scan and build Intlayer declaration files into dictionaries based on Intlayer configuration.

300 lines 11.4 kB
import generator from "@babel/generator"; import * as babelParser from "@babel/parser"; import traverse from "@babel/traverse"; import * as t from "@babel/types"; import { getAppLogger, logger } from "@intlayer/config"; import configuration from "@intlayer/config/built"; import { NodeType } from "@intlayer/core"; import { existsSync } from "fs"; import { readFile, writeFile } from "fs/promises"; import { extname } from "path"; import { getContentDeclarationFileTemplate } from "../getContentDeclarationFileTemplate/getContentDeclarationFileTemplate.mjs"; import { formatCode } from "./formatCode.mjs"; const writeJSFile = async (filePath, dictionary) => { const appLogger = getAppLogger(configuration); const { key: dictionaryIdentifierKey, content: updatesToApply, locale, autoFilled } = dictionary; const isPerLocaleDeclarationFile = typeof locale === "string"; if (!existsSync(filePath)) { const fileExtension = extname(filePath); let format = "ts"; if (fileExtension === ".ts" || fileExtension === ".tsx") { format = "ts"; } else if (fileExtension === ".cjs" || fileExtension === ".cjsx") { format = "cjs"; } else { format = "esm"; } appLogger("File does not exist, creating it", { isVerbose: true }); const template = await getContentDeclarationFileTemplate( dictionaryIdentifierKey, format, { locale, autoFilled } ); await writeFile(filePath, template, "utf-8"); } let sourceCode; try { sourceCode = await readFile(filePath, "utf-8"); } catch (error) { const err = error; appLogger(`Failed to read file: ${filePath}`, { level: "error" }); throw new Error(`Failed to read file ${filePath}: ${err.message}`); } const ast = babelParser.parse(sourceCode, { sourceType: "module", plugins: ["typescript", "jsx"], tokens: true }); let dictionaryObjectPath = null; let dictionaryIdentifier = null; traverse(ast, { ObjectExpression(path) { if (dictionaryObjectPath) return; const keyProp = path.node.properties.find((prop) => { if (!t.isObjectProperty(prop)) return false; if (!t.isIdentifier(prop.key) && !t.isStringLiteral(prop.key)) return false; const keyName = t.isIdentifier(prop.key) ? prop.key.name : prop.key.value; if (keyName !== "key" || !t.isStringLiteral(prop.value)) return false; const propValue = prop.value.value; return propValue === dictionaryIdentifierKey; }); if (keyProp) { dictionaryObjectPath = path; path.stop(); } } }); if (!dictionaryObjectPath) { appLogger(`Looking for variable declarations`, { isVerbose: true }); const candidateVars = []; traverse(ast, { VariableDeclarator(path) { const { node } = path; if (!t.isIdentifier(node.id)) return; let objPath = null; if (node.init && t.isObjectExpression(node.init)) { objPath = path.get("init"); } else if (node.init && (t.isTSAsExpression(node.init) || t.isTSTypeAssertion(node.init)) && t.isObjectExpression(node.init.expression)) { objPath = path.get("init.expression"); } if (objPath) { candidateVars.push({ id: node.id.name, path: objPath }); } } }); appLogger(`Found ${candidateVars.length} candidate variables`, { isVerbose: true }); for (const { id, path } of candidateVars) { const keyProp = path.node.properties.find((prop) => { if (!t.isObjectProperty(prop)) return false; if (!t.isIdentifier(prop.key) && !t.isStringLiteral(prop.key)) return false; const keyName = t.isIdentifier(prop.key) ? prop.key.name : prop.key.value; return keyName === "key" && t.isStringLiteral(prop.value) && prop.value.value === dictionaryIdentifierKey; }); if (keyProp) { appLogger(`Found match in variable: ${id}`); dictionaryObjectPath = path; dictionaryIdentifier = id; break; } } if (!dictionaryObjectPath) { appLogger("Could not find dictionary object. Dumping all objects:", { isVerbose: true }); traverse(ast, { ObjectExpression(path) { const props = path.node.properties.map((prop) => { if (!t.isObjectProperty(prop)) return "non-object-property"; if (!t.isIdentifier(prop.key) && !t.isStringLiteral(prop.key)) return "complex-key"; const keyName = t.isIdentifier(prop.key) ? prop.key.name : prop.key.value; let valueDesc = "unknown-value"; if (t.isStringLiteral(prop.value)) { valueDesc = `"${prop.value.value}"`; } return `${keyName}: ${valueDesc}`; }).join(", "); appLogger(`Object: { ${props} }`); } }); } } if (!dictionaryObjectPath) { throw new Error( `Could not find dictionary object with key '${dictionaryIdentifierKey}' in ${filePath}` ); } const contentPropertyPath = dictionaryObjectPath.get("properties").find((propPath) => { if (!propPath.isObjectProperty()) return false; const propNode = propPath.node; const keyName = t.isIdentifier(propNode.key) ? propNode.key.name : t.isStringLiteral(propNode.key) ? propNode.key.value : null; return keyName === "content"; }); if (!contentPropertyPath || !contentPropertyPath.isObjectProperty() || !contentPropertyPath.get("value").isObjectExpression()) { throw new Error( `Could not find 'content' object property, or it's not an object, in dictionary in ${filePath}` ); } const contentObjectPath = contentPropertyPath.get( "value" ); const buildValueNodeFromData = (data) => { if (data?.nodeType === NodeType.Translation) { const translationContent = data; if (isPerLocaleDeclarationFile && typeof locale === "string" && translationContent?.[NodeType.Translation]?.[locale] !== void 0) { return buildValueNodeFromData( translationContent[NodeType.Translation][locale] ); } const translationsObj = t.objectExpression( Object.entries(translationContent?.[NodeType.Translation] ?? {}).map( ([langKey, langValue]) => { const keyNode = t.isValidIdentifier(langKey) ? t.identifier(langKey) : t.stringLiteral(langKey); return t.objectProperty(keyNode, buildValueNodeFromData(langValue)); } ) ); return t.callExpression(t.identifier("t"), [translationsObj]); } if (Array.isArray(data)) { return t.arrayExpression(data.map((el) => buildValueNodeFromData(el))); } if (data && typeof data === "object") { const props = Object.entries(data).map(([k, v]) => { const key = t.isValidIdentifier(k) ? t.identifier(k) : t.stringLiteral(k); return t.objectProperty(key, buildValueNodeFromData(v)); }); return t.objectExpression(props); } switch (typeof data) { case "string": return t.stringLiteral(data); case "number": return t.numericLiteral(data); case "boolean": return t.booleanLiteral(data); default: return t.nullLiteral(); } }; const ensureObjectProperty = (objPath, key) => { const existing = objPath.get("properties").find((propPath) => { if (!propPath.isObjectProperty()) return false; const propNode = propPath.node; const keyName = t.isIdentifier(propNode.key) ? propNode.key.name : t.isStringLiteral(propNode.key) ? propNode.key.value : null; return keyName === key; }); if (existing) return existing; const keyNode = t.isValidIdentifier(key) ? t.identifier(key) : t.stringLiteral(key); const newProp = t.objectProperty(keyNode, t.objectExpression([])); objPath.node.properties.push(newProp); const props = objPath.get("properties"); return props[props.length - 1]; }; const mergeValueIntoProperty = (propPath, value, propKeyForLogs) => { const valuePath = propPath.get("value"); if (value?.nodeType === NodeType.Translation) { const translationContent = value; if (valuePath.isCallExpression() && t.isIdentifier(valuePath.node.callee) && valuePath.node.callee.name === "t") { const translationsObj = t.objectExpression( Object.entries(translationContent?.[NodeType.Translation] ?? {}).map( ([langKey, langValue]) => t.objectProperty( t.isValidIdentifier(langKey) ? t.identifier(langKey) : t.stringLiteral(langKey), buildValueNodeFromData(langValue) ) ) ); if (isPerLocaleDeclarationFile && typeof locale === "string") { const localized = translationContent?.[NodeType.Translation]?.[locale]; if (localized !== void 0) { valuePath.replaceWith(buildValueNodeFromData(localized)); return; } } valuePath.node.arguments = [translationsObj]; return; } valuePath.replaceWith(buildValueNodeFromData(value)); return; } if (Array.isArray(value)) { valuePath.replaceWith(buildValueNodeFromData(value)); return; } if (value && typeof value === "object") { if (!valuePath.isObjectExpression()) { valuePath.replaceWith(t.objectExpression([])); } const objPath = valuePath; for (const [k, v] of Object.entries(value)) { const childProp = ensureObjectProperty(objPath, k); mergeValueIntoProperty(childProp, v, `${propKeyForLogs}.${k}`); } return; } valuePath.replaceWith(buildValueNodeFromData(value)); return; }; for (const [entryKeyToUpdate, newEntryData] of Object.entries( updatesToApply )) { let targetPropertyPath = contentObjectPath.get("properties").find((propPath) => { if (!propPath.isObjectProperty()) return false; const propNode = propPath.node; const keyName = t.isIdentifier(propNode.key) ? propNode.key.name : t.isStringLiteral(propNode.key) ? propNode.key.value : null; return keyName === entryKeyToUpdate; }); if (!targetPropertyPath) { const keyNode = t.isValidIdentifier(entryKeyToUpdate) ? t.identifier(entryKeyToUpdate) : t.stringLiteral(entryKeyToUpdate); const newProperty = t.objectProperty(keyNode, t.objectExpression([])); contentObjectPath.node.properties.push(newProperty); const props = contentObjectPath.get("properties"); targetPropertyPath = props[props.length - 1]; } mergeValueIntoProperty(targetPropertyPath, newEntryData, entryKeyToUpdate); } const generatedCode = generator(ast, { retainLines: true, comments: true, jsescOption: { minimal: true // This ensures Unicode characters are not escaped } }).code; let finalCode = generatedCode; finalCode = await formatCode(filePath, finalCode); try { await writeFile(filePath, finalCode, "utf-8"); logger(`Successfully updated ${filePath}`, { level: "info", isVerbose: true }); } catch (error) { const err = error; logger(`Failed to write updated file: ${filePath}`, { level: "error" }); throw new Error(`Failed to write updated file ${filePath}: ${err.message}`); } }; export { writeJSFile }; //# sourceMappingURL=writeJSFile.mjs.map