import { isArray, isEqual, isObject, isString } from 'lodash'; import { StringSegment, getDynamicStringSegments, isExactBinding, joinStringWithBindings } from '../bindings/index.js'; import { LanguagePluginID } from '../types/plugin/language.js'; import { Extractor } from './types.js'; export type ScopedDataTree = { [entityName: string]: unknown }; const splitStringWithBindings = (value: string): StringSegment[] => { const stringSegments = getDynamicStringSegments(value.trim()); return stringSegments.map((segment) => { const length = segment.length; if (isExactBinding(segment)) { return { dynamic: true, value: segment.substring(2, length - 2) }; } return { dynamic: false, value: segment }; }); }; export async function refactorNameInCode({ value, oldName, newName, dataTree, extractPairs, suffixes, namespace }: { value: string; oldName: string; newName: string; dataTree: ScopedDataTree; extractPairs: Extractor; suffixes?: Array; namespace?: string; }) { let pairs; if (namespace) { const namespacedEntitiesToExtract = { [namespace]: new Set([oldName]) }; pairs = await extractPairs(value, new Set(), dataTree, namespacedEntitiesToExtract); } else { pairs = await extractPairs(value, new Set([oldName]), dataTree); } let position = 0; let result = ''; for (const { binding, range } of pairs) { if (suffixes) { // only refactor if the code after the binding matches a suffix const restOfCode = value.slice(range.start + binding.length); if (!suffixes.some((suffix) => restOfCode.startsWith(suffix))) { result += value.slice(position, range.end + 1); position = range.end + 1; continue; } } result += value.slice(position, range.start) + newName; position = range.start + binding.length; } return result + value.slice(position); } async function refactorNameInSegment({ segment, oldName, newName, dataTree, extractPairs, namespace }: { segment: StringSegment; oldName: string; newName: string; dataTree: ScopedDataTree; extractPairs: Extractor; namespace?: string; }) { if (segment.dynamic) { const value = await refactorNameInCode({ value: segment.value, oldName, newName, dataTree, extractPairs, namespace }); if (value === null) { return null; } return { dynamic: true, value }; } return segment; } export async function refactorNameInString({ value, oldName, newName, dataTree, extractPairs, namespace }: { value: string; oldName: string; newName: string; dataTree: ScopedDataTree; extractPairs: Extractor; namespace?: string; }) { const segments = splitStringWithBindings(value); const newSegments = await Promise.all( segments.map((segment) => refactorNameInSegment({ segment, oldName, newName, dataTree, extractPairs, namespace })) ); if (newSegments.filter((segment) => segment === null).length) { return value; } return joinStringWithBindings(newSegments.filter((segment): segment is StringSegment => segment !== null)); } export async function refactorNameInValue({ oldValue, oldName, newName, dataTree, extractPairs, namespace, includeNestedObjects }: { oldValue: unknown; oldName: string; newName: string; dataTree: ScopedDataTree; extractPairs: Extractor; namespace?: string; includeNestedObjects?: boolean; }) { if (isArray(oldValue)) { return Promise.all( oldValue.map(async (value) => { if (typeof value === 'string') { return refactorNameInString({ value, oldName, newName, dataTree, extractPairs, namespace }); } else if (includeNestedObjects && value !== null && typeof value === 'object') { return refactorNameInValue({ oldValue: value, oldName, newName, dataTree, extractPairs, namespace, includeNestedObjects: true }); } else { return value; } }) ); } else if (isString(oldValue)) { return refactorNameInString({ value: oldValue, oldName, newName, dataTree, extractPairs, namespace }); } else if (isObject(oldValue)) { const entries = await Promise.all( Object.entries(oldValue).map(async ([key, value]) => { const newValue: unknown = await refactorNameInValue({ oldValue: value, oldName, newName, dataTree, extractPairs, namespace, includeNestedObjects }); return [key, newValue]; }) ); return Object.fromEntries(entries); } return oldValue; } export function createFakeDataTree(dsls: Record[]): Record { const fakeDataTree = {}; for (const dsl of dsls) { Object.values(dsl).forEach((entity) => { const name = entity.name || entity.widgetName; if (!name) { return; } fakeDataTree[name] = {}; }); } return fakeDataTree; } export async function refactorNameInStep({ oldName, newName, dataTree, suffixes, namespace, configuration, pluginId, extractor }: { oldName: string; newName: string; dataTree: ScopedDataTree; suffixes?: string[]; namespace?: string; pluginId?: string; configuration: Record; extractor: Extractor; }) { for (const [key, value] of Object.entries(configuration)) { if (isString(value)) { let newValue: string | null = null; // Code body's vs plain text bodies (JS, Python vs HTTP, etc) if (key === 'body' && (pluginId === LanguagePluginID.JavaScript || pluginId === LanguagePluginID.Python)) { newValue = await refactorNameInCode({ value, oldName, newName, dataTree, extractPairs: extractor, suffixes, namespace }); } else { newValue = await refactorNameInString({ value, oldName, newName, dataTree, extractPairs: extractor, namespace }); } if (!isEqual(newValue, value)) { configuration[key] = newValue; } } else if (typeof value === 'object' && value !== null) { configuration[key] = await refactorNameInValue({ oldValue: value, oldName, newName, dataTree, extractPairs: extractor, namespace, includeNestedObjects: true }); } } }