1import fs from 'fs-extra'; 2import inquirer from 'inquirer'; 3import minimatch from 'minimatch'; 4import path from 'path'; 5 6import { printDiff } from './Diff'; 7import { 8 CopyFileOptions, 9 CopyFileResult, 10 FileTransform, 11 RawTransform, 12 ReplaceTransform, 13 StringTransform, 14} from './Transforms.types'; 15import { arrayize } from './Utils'; 16 17export * from './Transforms.types'; 18 19function isRawTransform(transform: any): transform is RawTransform { 20 return transform.transform; 21} 22 23function isReplaceTransform(transform: any): transform is ReplaceTransform { 24 return transform.find !== undefined && transform.replaceWith !== undefined; 25} 26 27/** 28 * Transforms input string according to the given transform rules. 29 */ 30export function transformString( 31 input: string, 32 transforms: StringTransform[] | null | undefined 33): string { 34 if (!transforms) { 35 return input; 36 } 37 return transforms.reduce((acc, transform) => { 38 return applySingleTransform(acc, transform); 39 }, input); 40} 41 42async function getTransformedFileContentAsync( 43 filePath: string, 44 transforms: FileTransform[] 45): Promise<string> { 46 let result = await fs.readFile(filePath, 'utf8'); 47 for (const transform of transforms) { 48 const beforeTransformation = result; 49 result = applySingleTransform(result, transform); 50 await maybePrintDebugInfoAsync(beforeTransformation, result, filePath, transform); 51 } 52 return result; 53} 54 55async function maybePrintDebugInfoAsync( 56 contentBefore: string, 57 contentAfter: string, 58 filePath: string, 59 transform: FileTransform 60): Promise<void> { 61 if (!transform.debug || contentAfter === contentBefore) { 62 return; 63 } 64 const transformName = 65 typeof transform.debug === 'string' ? transform.debug : JSON.stringify(transform, null, 2); 66 67 printDiff(contentBefore, contentAfter); 68 69 const { isCorrect } = await inquirer.prompt<{ isCorrect: boolean }>([ 70 { 71 type: 'confirm', 72 name: 'isCorrect', 73 message: `Changes in file ${filePath} introduced by transform ${transformName}`, 74 default: true, 75 }, 76 ]); 77 if (!isCorrect) { 78 throw new Error('ABORTING'); 79 } 80} 81 82function applySingleTransform(input: string, transform: StringTransform): string { 83 if (isRawTransform(transform)) { 84 return transform.transform(input); 85 } else if (isReplaceTransform(transform)) { 86 const { find, replaceWith } = transform; 87 // @ts-ignore @tsapeta: TS gets crazy on `replaceWith` being a function. 88 return input.replace(find, replaceWith); 89 } 90 throw new Error(`Unknown transform type`); 91} 92 93/** 94 * Transforms file's content in-place. 95 */ 96export async function transformFileAsync( 97 filePath: string, 98 transforms: StringTransform[] | null | undefined 99): Promise<void> { 100 const content = await fs.readFile(filePath, 'utf8'); 101 await fs.outputFile(filePath, transformString(content, transforms)); 102} 103 104/** 105 * Copies a file from source directory to target directory with transformed relative path and content. 106 */ 107export async function copyFileWithTransformsAsync( 108 options: CopyFileOptions 109): Promise<CopyFileResult> { 110 const { sourceFile, sourceDirectory, targetDirectory, transforms } = options; 111 const sourcePath = path.join(sourceDirectory, sourceFile); 112 113 // Transform the target path according to rename rules. 114 const targetFile = transformString(sourceFile, transforms.path); 115 const targetPath = path.join(targetDirectory, targetFile); 116 117 // Filter out transforms that don't match paths patterns. 118 const filteredContentTransforms = 119 transforms.content?.filter( 120 ({ paths }) => 121 !paths || 122 arrayize(paths).some((pattern) => minimatch(sourceFile, pattern, { matchBase: true })) 123 ) ?? []; 124 125 // Transform source content. 126 const content = await getTransformedFileContentAsync(sourcePath, filteredContentTransforms); 127 128 // Save transformed source file at renamed target path. 129 await fs.outputFile(targetPath, content); 130 131 return { 132 content, 133 targetFile, 134 }; 135} 136