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