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 // Transform source content. 47 let result = await fs.readFile(filePath, 'utf8'); 48 for (const transform of transforms) { 49 const beforeTransformation = result; 50 result = applySingleTransform(result, transform); 51 await maybePrintDebugInfoAsync(beforeTransformation, result, filePath, transform); 52 } 53 return result; 54} 55 56/** 57 * Filters file contents transformations to only these that match path patterns. 58 * `filePath` param should be a relative path. 59 */ 60function getFilteredContentTransforms(transforms: FileTransform[], filePath: string) { 61 return transforms.filter( 62 ({ paths }) => 63 !paths || arrayize(paths).some((pattern) => minimatch(filePath, pattern, { matchBase: true })) 64 ); 65} 66 67async function maybePrintDebugInfoAsync( 68 contentBefore: string, 69 contentAfter: string, 70 filePath: string, 71 transform: FileTransform 72): Promise<void> { 73 if (!transform.debug || contentAfter === contentBefore) { 74 return; 75 } 76 const transformName = 77 typeof transform.debug === 'string' ? transform.debug : JSON.stringify(transform, null, 2); 78 79 printDiff(contentBefore, contentAfter); 80 81 const { isCorrect } = await inquirer.prompt<{ isCorrect: boolean }>([ 82 { 83 type: 'confirm', 84 name: 'isCorrect', 85 message: `Changes in file ${filePath} introduced by transform ${transformName}`, 86 default: true, 87 }, 88 ]); 89 if (!isCorrect) { 90 throw new Error('ABORTING'); 91 } 92} 93 94function applySingleTransform(input: string, transform: StringTransform): string { 95 if (isRawTransform(transform)) { 96 return transform.transform(input); 97 } else if (isReplaceTransform(transform)) { 98 const { find, replaceWith } = transform; 99 // @ts-ignore @tsapeta: TS gets crazy on `replaceWith` being a function. 100 return input.replace(find, replaceWith); 101 } 102 throw new Error(`Unknown transform type`); 103} 104 105/** 106 * Transforms file's content in-place. 107 */ 108export async function transformFileAsync( 109 filePath: string, 110 transforms: StringTransform[] | null | undefined 111): Promise<void> { 112 const content = await fs.readFile(filePath, 'utf8'); 113 await fs.outputFile(filePath, transformString(content, transforms)); 114} 115 116/** 117 * Transforms multiple files' content in-place. 118 */ 119export async function transformFilesAsync(files: string[], transforms: FileTransform[]) { 120 for (const file of files) { 121 const filteredContentTransforms = getFilteredContentTransforms(transforms ?? [], file); 122 123 // Transform source content. 124 const content = await getTransformedFileContentAsync(file, filteredContentTransforms); 125 126 // Save transformed content 127 await fs.outputFile(file, content); 128 } 129} 130 131/** 132 * Copies a file from source directory to target directory with transformed relative path and content. 133 */ 134export async function copyFileWithTransformsAsync( 135 options: CopyFileOptions 136): Promise<CopyFileResult> { 137 const { sourceFile, sourceDirectory, targetDirectory, transforms } = options; 138 const sourcePath = path.join(sourceDirectory, sourceFile); 139 140 // Transform the target path according to rename rules. 141 const targetFile = transformString(sourceFile, transforms.path); 142 const targetPath = path.join(targetDirectory, targetFile); 143 144 const filteredContentTransforms = getFilteredContentTransforms( 145 transforms.content ?? [], 146 sourceFile 147 ); 148 149 // Transform source content. 150 const content = await getTransformedFileContentAsync(sourcePath, filteredContentTransforms); 151 152 if (filteredContentTransforms.length > 0) { 153 // Save transformed source file at renamed target path. 154 await fs.outputFile(targetPath, content); 155 } else { 156 // When there are no transforms, it's safer to just copy the file. 157 // Only then binary files will be copied properly since the `content` is utf8-encoded. 158 await fs.copy(sourcePath, targetPath, { overwrite: true }); 159 } 160 161 // Keep original file mode if needed. 162 if (options.keepFileMode) { 163 const { mode } = await fs.stat(sourcePath); 164 await fs.chmod(targetPath, mode); 165 } 166 167 return { 168 content, 169 targetFile, 170 }; 171} 172