/* eslint-disable quotes */ // Webruntime requires all labels to be provided in a src/storybook-config/core-ui-labels.json file. // In our case this labels.json file needs to include the labels we specify in our projects // and the labels referenced in any other projects we use (e.g. ui-b2b-components/ui-lightning-community/...). // The source of thruth for ui-b2b-components and ui-lightning-community labels is in the shared core labels folder: // https://codesearch.data.sfdc.net/source/xref/app_main_core/app/main/core/shared-labels/java/resources/sfdc/i18n/shared_core_ui_labels/ // To be consistent we also make the shared core labels folder our source of truth for labels. // This script auto-generates the src/storybook-config/core-ui-labels.json file. // The script uses the @lwc/module-resolver to find out all the modules we need and scans them for // @salesforce/label references. It then goes over the shared core labels folders to find the // mappings for those label references and adds all of them to the src/storybook-config/core-ui-labels.json file /* eslint-disable no-console */ import fs from 'fs'; import path from 'path'; import readline from 'readline'; import { resolveModules } from '../utils/resolver'; import parser from 'fast-xml-parser'; import colors from 'colors'; interface StringMap { [key: string]: string; } const { log, warn } = console; const coreHome = process.env.CORE_HOME ? String(process.env.CORE_HOME) : path.join(String(process.env.HOME), 'blt/app/main/core'); const coreLabelsDir = path.join(coreHome, 'shared-labels/java/resources/sfdc/i18n/shared_core_ui_labels'); const labelReferences = new Set(); const analyzedFiles = new Set(); /** * @return Set of all labelReferences used in modules referenced from the project */ async function findLabelReferences(projectDir: string) { const modules = resolveModules(projectDir); for (const mod of modules) { // eslint-disable-next-line no-await-in-loop await addLabelReferencesInFile(mod.entry); } } async function addLabelReferencesInFile(file: string) { if (analyzedFiles.has(file)) { return; } analyzedFiles.add(file); const rl = readline.createInterface({ input: fs.createReadStream(file), }); const fileType = path.extname(file); // eslint-disable-next-line no-await-in-loop for await (let line of rl) { line = line.trim(); if ( line.startsWith('import') || line.startsWith('} from') // e.g.: } from './resultsNormalizer'; ) { let start = line.indexOf('@salesforce/label/'); if (start !== -1) { const delim = line.charAt(start - 1); start += 18; const labelReference = line.substring(start, line.indexOf(delim, start)); labelReferences.add(labelReference); } else { // need to handle imported files, e.g.: import * as labels from './labels.js'; let end = line.lastIndexOf("'"); if (end === -1) { end = line.lastIndexOf('"'); } if (end !== -1) { const delim = line.charAt(end); let importPath = line.substring(line.lastIndexOf(delim, end - 1) + 1, end); if (importPath.startsWith('./')) { const suffix = path.extname(importPath); if (!suffix || ['.js', '.ts'].includes(suffix)) { if (!suffix) { importPath += fileType || '.js'; } const importFile = path.resolve(path.dirname(file), importPath); if (fs.existsSync(importFile)) { addLabelReferencesInFile(importFile); } } } } } } } } /** * Goes over all xml files in coreLabelsDir to find the labels for the labelReferences * * @return labels.json object */ async function findLabelsInCore() { const labels: any = { addLabel(namespace: string, labelName: string, label: string) { if (!this[namespace]) { this[namespace] = {}; } this[namespace][labelName] = label; }, }; fs.readdirSync(coreLabelsDir).forEach(file => { if (file.endsWith('.xml')) { const sections = parseLabelsXml(path.join(coreLabelsDir, file)); if (sections) { for (const section of sections) { const namespace = section['@_name']; const params = section.param; if (params) { for (const param of section.param) { const labelName = param['@_name']; const labelReference = `${namespace}.${labelName}`; if (labelReferences.delete(labelReference)) { const label = param['#text']; labels.addLabel(namespace, labelName, label); } } } } } } }); return labels; } const parserOptions = { ignoreAttributes: false, ignoreNameSpace: false, allowBooleanAttributes: false, parseNodeValue: false, parseAttributeValue: false, trimValues: true, arrayMode: true, }; /** * @return sections array */ function parseLabelsXml(file: string): any { const xml = fs.readFileSync(file, 'utf-8'); const parsed = parser.parse(xml, parserOptions); return parsed.iniFile && parsed.iniFile[0] && parsed.iniFile[0].section; } export async function buildLabelsJson(labelsPath: string = 'src/storybook-config/core-ui-labels.json') { log(colors.blue(`\tbuild labels`)); log(colors.blue(`\t\tfrom: `) + coreLabelsDir); const labelParentDir = labelsPath.substring(0, labelsPath.lastIndexOf('/')); if (!fs.existsSync(labelParentDir)) { fs.mkdirSync(labelParentDir, { recursive: true }); } const labelJsonFile = path.resolve(process.cwd(), labelsPath); await findLabelReferences('.'); log(`\t\tfound ${labelReferences.size} label references in ${analyzedFiles.size} files`); const labels = await findLabelsInCore(); if (labelReferences.size > 0) { warn(colors.yellow('\t\tWARNING, no core labels found for:')); for (const _labelReference of labelReferences) { const labelReference: string = _labelReference as string; warn(colors.yellow(`\t\t\t${labelReference}`)); const dot = labelReference.indexOf('.'); const namespace = labelReference.substring(0, dot); const labelName = labelReference.substring(dot + 1); // add dummy labels so webruntime doesn't crash labels.addLabel(namespace, labelName, labelReference); } } fs.writeFileSync(labelJsonFile, JSON.stringify(labels, null, 4) + '\n'); log(colors.blue(`\t\tto: `) + labelJsonFile); }