@vendure/ngx-translate-extract
Version:
Extract strings from projects using ngx-translate
243 lines (242 loc) • 10.3 kB
JavaScript
import { extname } from 'node:path';
import { parseTemplate } from '@angular/compiler';
import { ScriptKind, tsquery } from '@phenomnomnominal/tsquery';
import pkg from 'typescript';
const { isArrayLiteralExpression, isBinaryExpression, isCallExpression, isConditionalExpression, isPropertyAccessExpression, isStringLiteralLike, SyntaxKind, } = pkg;
const ANGULAR_TEMPLATE_KIND = ScriptKind.Unknown;
const SCRIPT_TYPES = new Map([
['.js', ScriptKind.JS],
['.mjs', ScriptKind.JS],
['.jsx', ScriptKind.JSX],
['.ts', ScriptKind.TS],
['.mts', ScriptKind.TS],
['.tsx', ScriptKind.TSX],
['.html', ANGULAR_TEMPLATE_KIND],
]);
const AST_CACHE = new Map();
export function clearAstCache() {
AST_CACHE.clear();
}
export function getAST(source, fileName = '') {
if (!fileName) {
return parseSource(source, fileName);
}
const cached = AST_CACHE.get(fileName);
if (cached && cached.source === source) {
return cached;
}
const result = parseSource(source, fileName);
AST_CACHE.set(fileName, result);
return result;
}
export function parseSource(source, fileName = '') {
const scriptKind = SCRIPT_TYPES.get(extname(fileName)) ?? ScriptKind.TS;
if (scriptKind === ANGULAR_TEMPLATE_KIND) {
return {
source,
parsedFile: null,
parsedTemplates: [parseTemplate(source, fileName, { collectCommentNodes: false })],
};
}
const parsedFile = tsquery.ast(source, fileName, scriptKind);
const parsedTemplates = [];
if (source.includes('@Component(') && source.includes('template:')) {
getComponentInlineTemplate(parsedFile).forEach((templateNode) => {
const tpl = templateNode.initializer;
if (isStringLiteralLike(tpl)) {
parsedTemplates.push(parseTemplate(tpl.text, fileName, { collectCommentNodes: false }));
}
});
}
return { source, parsedFile, parsedTemplates };
}
export function getComponentInlineTemplate(node) {
const query = 'Decorator > CallExpression:has(Identifier[name="Component"]) ObjectLiteralExpression > PropertyAssignment:has(Identifier[name="template"])';
return tsquery(node, query);
}
export function getNamedImportIdentifiers(node, moduleName, importPath) {
const importStringLiteralValue = importPath instanceof RegExp ? `value=${importPath.toString()}` : `value="${importPath}"`;
const query = `ImportDeclaration:has(StringLiteral[${importStringLiteralValue}]) ImportSpecifier:has(Identifier[name="${moduleName}"]) > Identifier`;
return tsquery(node, query);
}
export function getNamedImport(node, importName, importPath) {
const identifiers = getNamedImportIdentifiers(node, importName, importPath);
return identifiers.at(0)?.text ?? null;
}
export function getNamedImportAlias(node, importName, importPath) {
const identifiers = getNamedImportIdentifiers(node, importName, importPath);
return identifiers.at(-1)?.text ?? null;
}
export function findClassDeclarations(node, name) {
let query = 'ClassDeclaration';
if (name) {
query += `:has(Identifier[name="${name}"])`;
}
return tsquery(node, query);
}
export function findFunctionExpressions(node) {
return tsquery(node, 'VariableDeclaration ArrowFunction, VariableDeclaration FunctionExpression');
}
export function getSuperClassName(node) {
const query = 'ClassDeclaration > HeritageClause Identifier';
const [result] = tsquery(node, query);
return result?.text ?? null;
}
export function getImportPath(node, className) {
const query = `ImportDeclaration:has(Identifier[name="${className}"]) StringLiteral`;
const [result] = tsquery(node, query);
return result?.text ?? null;
}
export function findClassPropertiesByType(node, type) {
return [
...findClassPropertiesConstructorParameterByType(node, type),
...findClassPropertiesDeclarationByType(node, type),
...findClassPropertiesDeclarationByInject(node, type),
...findClassPropertiesGetterByType(node, type),
];
}
export function findConstructorDeclaration(node) {
const query = 'Constructor';
const [result] = tsquery(node, query);
return result;
}
export function findMethodParameterByType(node, type) {
const query = `Parameter:has(TypeReference > Identifier[name="${type}"]) > Identifier`;
const [result] = tsquery(node, query);
if (result) {
return result.text;
}
return null;
}
export function findVariableNameByInjectType(node, type) {
const query = `VariableDeclaration:has(Identifier[name="inject"]):has(CallExpression > Identifier[name="${type}"]) > Identifier`;
const [result] = tsquery(node, query);
return result?.text ?? null;
}
export function findMethodCallExpressions(node, propName, fnName) {
const functionNames = typeof fnName === 'string' ? [fnName] : fnName;
const fnNameRegex = functionNames.join('|');
const query = `CallExpression > PropertyAccessExpression:has(Identifier[name=/^(${fnNameRegex})$/]):has(PropertyAccessExpression:has(Identifier[name="${propName}"]):not(:has(ThisKeyword)))`;
return unwrapMatchedCallExpressions(tsquery(node, query), functionNames);
}
export function findInlineInjectCallExpressions(node, injectType, fnName) {
const functionNames = typeof fnName === 'string' ? [fnName] : fnName;
const fnNameRegex = functionNames.join('|');
const query = `CallExpression > PropertyAccessExpression:has(Identifier[name=/^(${fnNameRegex})$/]):has(CallExpression:has(Identifier[name="inject"]):has(Identifier[name="${injectType}"]))`;
return unwrapMatchedCallExpressions(tsquery(node, query), functionNames);
}
export function findClassPropertiesConstructorParameterByType(node, type) {
const query = `Constructor Parameter:has(TypeReference > Identifier[name="${type}"]):has(PublicKeyword,ProtectedKeyword,PrivateKeyword,ReadonlyKeyword) > Identifier`;
const result = tsquery(node, query);
return result.map((n) => n.text);
}
export function findClassPropertiesDeclarationByType(node, type) {
const query = `PropertyDeclaration:has(TypeReference > Identifier[name="${type}"])`;
const result = tsquery(node, query);
return result.map((n) => n.name.getText());
}
export function findClassPropertiesDeclarationByInject(node, type) {
const query = `PropertyDeclaration:has(CallExpression > Identifier[name="inject"]):has(CallExpression > Identifier[name="${type}"])`;
const result = tsquery(node, query);
return result.map((n) => n.name.getText());
}
export function findClassPropertiesGetterByType(node, type) {
const query = `GetAccessor:has(TypeReference > Identifier[name="${type}"]) > Identifier`;
const result = tsquery(node, query);
return result.map((n) => n.text);
}
export function findFunctionCallExpressions(node, fnName) {
if (Array.isArray(fnName)) {
fnName = fnName.join('|');
}
const query = `CallExpression:has(Identifier[name="${fnName}"]):not(:has(PropertyAccessExpression))`;
return tsquery(node, query);
}
export function findSimpleCallExpressions(node, fnName) {
if (Array.isArray(fnName)) {
fnName = fnName.join('|');
}
const query = `CallExpression:has(Identifier[name="${fnName}"])`;
return tsquery(node, query);
}
export function findPropertyCallExpressions(node, prop, fnName) {
if (Array.isArray(fnName)) {
fnName = fnName.join('|');
}
const query = `CallExpression > PropertyAccessExpression:has(Identifier[name=/^(${fnName})$/]):has(PropertyAccessExpression:has(ThisKeyword))`;
const result = tsquery(node, query);
const nodes = [];
result.forEach((n) => {
const identifier = isPropertyAccessExpression(n.expression) ? n.expression.name : null;
const property = identifier?.parent;
const method = property?.parent;
const callExpression = method?.parent;
if (identifier?.getText() === prop && callExpression && isCallExpression(callExpression)) {
nodes.push(callExpression);
}
});
return nodes;
}
export function getStringsFromExpression(expression) {
if (isStringLiteralLike(expression) && expression.text.trim() !== '') {
return [expression.text];
}
if (isArrayLiteralExpression(expression)) {
return expression.elements.flatMap(getStringsFromExpression);
}
if (isBinaryExpression(expression)) {
const [left] = getStringsFromExpression(expression.left);
const [right] = getStringsFromExpression(expression.right);
if (expression.operatorToken.kind === SyntaxKind.PlusToken) {
if (typeof left === 'string' && typeof right === 'string') {
return [left + right];
}
}
if (expression.operatorToken.kind === SyntaxKind.BarBarToken) {
const result = [];
if (typeof left === 'string') {
result.push(left);
}
if (typeof right === 'string') {
result.push(right);
}
return result;
}
}
if (isConditionalExpression(expression)) {
const [whenTrue] = getStringsFromExpression(expression.whenTrue);
const [whenFalse] = getStringsFromExpression(expression.whenFalse);
const result = [];
if (typeof whenTrue === 'string') {
result.push(whenTrue);
}
if (typeof whenFalse === 'string') {
result.push(whenFalse);
}
return result;
}
return [];
}
export function getNodesFromSwitchBlockTmpl(node) {
const groups = node.groups ?? ('cases' in node && node.cases);
if (!Array.isArray(groups)) {
return [];
}
return groups.flatMap((group) => {
if (!Array.isArray(group.children)) {
return [];
}
return group.children;
});
}
export function unwrapMatchedCallExpressions(nodes, fnNames) {
const fnNameSet = new Set(fnNames);
const results = [];
for (const n of nodes) {
const lastToken = n.getLastToken();
if (lastToken && fnNameSet.has(lastToken.getText()) && isCallExpression(n.parent)) {
results.push(n.parent);
}
}
return results;
}