xref: /expo/tools/src/Transforms.ts (revision 1a8a11e6)
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