@intlayer/chokidar
Version:
Uses chokidar to scan and build Intlayer declaration files into dictionaries based on Intlayer configuration.
300 lines • 11.4 kB
JavaScript
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