UNPKG

@vendure/ngx-translate-extract

Version:

Extract strings from projects using ngx-translate

243 lines (242 loc) 10.3 kB
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; }