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