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