19657025fSTomasz Sapetaimport fs from 'fs-extra'; 2f194f574SWojciech Kozyraimport inquirer from 'inquirer'; 39657025fSTomasz Sapetaimport minimatch from 'minimatch'; 49657025fSTomasz Sapetaimport path from 'path'; 59657025fSTomasz Sapeta 6f194f574SWojciech Kozyraimport { printDiff } from './Diff'; 723e91912SWojciech Kozyraimport { 823e91912SWojciech Kozyra CopyFileOptions, 923e91912SWojciech Kozyra CopyFileResult, 10f194f574SWojciech Kozyra FileTransform, 1123e91912SWojciech Kozyra RawTransform, 1223e91912SWojciech Kozyra ReplaceTransform, 1323e91912SWojciech Kozyra StringTransform, 1423e91912SWojciech Kozyra} from './Transforms.types'; 159657025fSTomasz Sapetaimport { arrayize } from './Utils'; 169657025fSTomasz Sapeta 179657025fSTomasz Sapetaexport * from './Transforms.types'; 189657025fSTomasz Sapeta 1923e91912SWojciech Kozyrafunction isRawTransform(transform: any): transform is RawTransform { 2023e91912SWojciech Kozyra return transform.transform; 2123e91912SWojciech Kozyra} 2223e91912SWojciech Kozyra 2323e91912SWojciech Kozyrafunction isReplaceTransform(transform: any): transform is ReplaceTransform { 2423e91912SWojciech Kozyra return transform.find !== undefined && transform.replaceWith !== undefined; 2523e91912SWojciech Kozyra} 2623e91912SWojciech Kozyra 279657025fSTomasz Sapeta/** 289657025fSTomasz Sapeta * Transforms input string according to the given transform rules. 299657025fSTomasz Sapeta */ 309657025fSTomasz Sapetaexport function transformString( 319657025fSTomasz Sapeta input: string, 329657025fSTomasz Sapeta transforms: StringTransform[] | null | undefined 339657025fSTomasz Sapeta): string { 349657025fSTomasz Sapeta if (!transforms) { 359657025fSTomasz Sapeta return input; 369657025fSTomasz Sapeta } 3723e91912SWojciech Kozyra return transforms.reduce((acc, transform) => { 38f194f574SWojciech Kozyra return applySingleTransform(acc, transform); 39f194f574SWojciech Kozyra }, input); 40f194f574SWojciech Kozyra} 41f194f574SWojciech Kozyra 42*1a8a11e6Saleqsioasync function applyTransformsOnFileContentAsync( 43f194f574SWojciech Kozyra filePath: string, 44f194c863STomasz Sapeta transforms: FileTransform[] 45*1a8a11e6Saleqsio): Promise<{ 46*1a8a11e6Saleqsio content: string; 47*1a8a11e6Saleqsio transformsUsed: Set<FileTransform>; 48*1a8a11e6Saleqsio}> { 499eae8a6aSKudo Chien // Transform source content. 50f194f574SWojciech Kozyra let result = await fs.readFile(filePath, 'utf8'); 51*1a8a11e6Saleqsio const transformsUsed = new Set<FileTransform>(); 52f194c863STomasz Sapeta for (const transform of transforms) { 53f194f574SWojciech Kozyra const beforeTransformation = result; 54f194f574SWojciech Kozyra result = applySingleTransform(result, transform); 55f194f574SWojciech Kozyra await maybePrintDebugInfoAsync(beforeTransformation, result, filePath, transform); 56*1a8a11e6Saleqsio if (result !== beforeTransformation) { 57*1a8a11e6Saleqsio transformsUsed.add(transform); 58f194f574SWojciech Kozyra } 59*1a8a11e6Saleqsio } 60*1a8a11e6Saleqsio return { content: result, transformsUsed }; 61f194f574SWojciech Kozyra} 62f194f574SWojciech Kozyra 63f194c863STomasz Sapeta/** 64f194c863STomasz Sapeta * Filters file contents transformations to only these that match path patterns. 65f194c863STomasz Sapeta * `filePath` param should be a relative path. 66f194c863STomasz Sapeta */ 67f194c863STomasz Sapetafunction getFilteredContentTransforms(transforms: FileTransform[], filePath: string) { 68f194c863STomasz Sapeta return transforms.filter( 69f194c863STomasz Sapeta ({ paths }) => 70f194c863STomasz Sapeta !paths || arrayize(paths).some((pattern) => minimatch(filePath, pattern, { matchBase: true })) 71f194c863STomasz Sapeta ); 72f194c863STomasz Sapeta} 73f194c863STomasz Sapeta 74f194f574SWojciech Kozyraasync function maybePrintDebugInfoAsync( 75f194f574SWojciech Kozyra contentBefore: string, 76f194f574SWojciech Kozyra contentAfter: string, 77f194f574SWojciech Kozyra filePath: string, 78f194f574SWojciech Kozyra transform: FileTransform 79f194f574SWojciech Kozyra): Promise<void> { 80f194f574SWojciech Kozyra if (!transform.debug || contentAfter === contentBefore) { 81f194f574SWojciech Kozyra return; 82f194f574SWojciech Kozyra } 83f194f574SWojciech Kozyra const transformName = 84f194f574SWojciech Kozyra typeof transform.debug === 'string' ? transform.debug : JSON.stringify(transform, null, 2); 85f194f574SWojciech Kozyra 86f194f574SWojciech Kozyra printDiff(contentBefore, contentAfter); 87f194f574SWojciech Kozyra 88f194f574SWojciech Kozyra const { isCorrect } = await inquirer.prompt<{ isCorrect: boolean }>([ 89f194f574SWojciech Kozyra { 90f194f574SWojciech Kozyra type: 'confirm', 91f194f574SWojciech Kozyra name: 'isCorrect', 92f194f574SWojciech Kozyra message: `Changes in file ${filePath} introduced by transform ${transformName}`, 93f194f574SWojciech Kozyra default: true, 94f194f574SWojciech Kozyra }, 95f194f574SWojciech Kozyra ]); 96f194f574SWojciech Kozyra if (!isCorrect) { 97f194f574SWojciech Kozyra throw new Error('ABORTING'); 98f194f574SWojciech Kozyra } 99f194f574SWojciech Kozyra} 100f194f574SWojciech Kozyra 101f194f574SWojciech Kozyrafunction applySingleTransform(input: string, transform: StringTransform): string { 10223e91912SWojciech Kozyra if (isRawTransform(transform)) { 103f194f574SWojciech Kozyra return transform.transform(input); 10423e91912SWojciech Kozyra } else if (isReplaceTransform(transform)) { 10523e91912SWojciech Kozyra const { find, replaceWith } = transform; 1069657025fSTomasz Sapeta // @ts-ignore @tsapeta: TS gets crazy on `replaceWith` being a function. 107f194f574SWojciech Kozyra return input.replace(find, replaceWith); 10823e91912SWojciech Kozyra } 10923e91912SWojciech Kozyra throw new Error(`Unknown transform type`); 1109657025fSTomasz Sapeta} 1119657025fSTomasz Sapeta 1129657025fSTomasz Sapeta/** 113c6ade495STomasz Sapeta * Transforms file's content in-place. 114c6ade495STomasz Sapeta */ 115c6ade495STomasz Sapetaexport async function transformFileAsync( 116c6ade495STomasz Sapeta filePath: string, 117c6ade495STomasz Sapeta transforms: StringTransform[] | null | undefined 118c6ade495STomasz Sapeta): Promise<void> { 119c6ade495STomasz Sapeta const content = await fs.readFile(filePath, 'utf8'); 120c6ade495STomasz Sapeta await fs.outputFile(filePath, transformString(content, transforms)); 121c6ade495STomasz Sapeta} 122c6ade495STomasz Sapeta 123c6ade495STomasz Sapeta/** 1249eae8a6aSKudo Chien * Transforms multiple files' content in-place. 1259eae8a6aSKudo Chien */ 126*1a8a11e6Saleqsioexport async function transformFilesAsync( 127*1a8a11e6Saleqsio files: string[], 128*1a8a11e6Saleqsio transforms: FileTransform[] 129*1a8a11e6Saleqsio): Promise<Set<FileTransform>> { 130*1a8a11e6Saleqsio const transformsUsed = new Set<FileTransform>(); 1319eae8a6aSKudo Chien for (const file of files) { 132f194c863STomasz Sapeta const filteredContentTransforms = getFilteredContentTransforms(transforms ?? [], file); 133f194c863STomasz Sapeta 1349eae8a6aSKudo Chien // Transform source content. 135*1a8a11e6Saleqsio const transformedFile = await applyTransformsOnFileContentAsync( 136*1a8a11e6Saleqsio file, 137*1a8a11e6Saleqsio filteredContentTransforms 138*1a8a11e6Saleqsio ); 139*1a8a11e6Saleqsio transformedFile.transformsUsed.forEach((transform) => transformsUsed.add(transform)); 1409eae8a6aSKudo Chien 1419eae8a6aSKudo Chien // Save transformed content 142*1a8a11e6Saleqsio await fs.outputFile(file, transformedFile.content); 1439eae8a6aSKudo Chien } 144*1a8a11e6Saleqsio return transformsUsed; 1459eae8a6aSKudo Chien} 1469eae8a6aSKudo Chien 1479eae8a6aSKudo Chien/** 1489657025fSTomasz Sapeta * Copies a file from source directory to target directory with transformed relative path and content. 1499657025fSTomasz Sapeta */ 1509657025fSTomasz Sapetaexport async function copyFileWithTransformsAsync( 1519657025fSTomasz Sapeta options: CopyFileOptions 1529657025fSTomasz Sapeta): Promise<CopyFileResult> { 1539657025fSTomasz Sapeta const { sourceFile, sourceDirectory, targetDirectory, transforms } = options; 1549657025fSTomasz Sapeta const sourcePath = path.join(sourceDirectory, sourceFile); 1559657025fSTomasz Sapeta 1569657025fSTomasz Sapeta // Transform the target path according to rename rules. 1579657025fSTomasz Sapeta const targetFile = transformString(sourceFile, transforms.path); 1589657025fSTomasz Sapeta const targetPath = path.join(targetDirectory, targetFile); 1599657025fSTomasz Sapeta 160f194c863STomasz Sapeta const filteredContentTransforms = getFilteredContentTransforms( 161f194c863STomasz Sapeta transforms.content ?? [], 162f194c863STomasz Sapeta sourceFile 163f194c863STomasz Sapeta ); 1649657025fSTomasz Sapeta 165f194c863STomasz Sapeta // Transform source content. 166*1a8a11e6Saleqsio const { content, transformsUsed } = await applyTransformsOnFileContentAsync( 167*1a8a11e6Saleqsio sourcePath, 168*1a8a11e6Saleqsio filteredContentTransforms 169*1a8a11e6Saleqsio ); 170f194c863STomasz Sapeta 171f194c863STomasz Sapeta if (filteredContentTransforms.length > 0) { 1729657025fSTomasz Sapeta // Save transformed source file at renamed target path. 1739657025fSTomasz Sapeta await fs.outputFile(targetPath, content); 174f194c863STomasz Sapeta } else { 175f194c863STomasz Sapeta // When there are no transforms, it's safer to just copy the file. 176f194c863STomasz Sapeta // Only then binary files will be copied properly since the `content` is utf8-encoded. 177f194c863STomasz Sapeta await fs.copy(sourcePath, targetPath, { overwrite: true }); 178f194c863STomasz Sapeta } 1799657025fSTomasz Sapeta 1804ab6f4c5SKudo Chien // Keep original file mode if needed. 1814ab6f4c5SKudo Chien if (options.keepFileMode) { 1824ab6f4c5SKudo Chien const { mode } = await fs.stat(sourcePath); 1834ab6f4c5SKudo Chien await fs.chmod(targetPath, mode); 1844ab6f4c5SKudo Chien } 1854ab6f4c5SKudo Chien 1869657025fSTomasz Sapeta return { 1879657025fSTomasz Sapeta content, 188*1a8a11e6Saleqsio transformsUsed, 1899657025fSTomasz Sapeta targetFile, 1909657025fSTomasz Sapeta }; 1919657025fSTomasz Sapeta} 192