xref: /expo/tools/src/Transforms.ts (revision fe1ef024)
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 applyTransformsOnFileContentAsync(
43  filePath: string,
44  transforms: FileTransform[]
45): Promise<{
46  content: string;
47  transformsUsed: Set<FileTransform>;
48}> {
49  // Transform source content.
50  let result = await fs.readFile(filePath, 'utf8');
51  const transformsUsed = new Set<FileTransform>();
52  for (const transform of transforms) {
53    const beforeTransformation = result;
54    result = applySingleTransform(result, transform);
55    await maybePrintDebugInfoAsync(beforeTransformation, result, filePath, transform);
56    if (result !== beforeTransformation) {
57      transformsUsed.add(transform);
58    }
59  }
60  return { content: result, transformsUsed };
61}
62
63/**
64 * Filters file contents transformations to only these that match path patterns.
65 * `filePath` param should be a relative path.
66 */
67function getFilteredContentTransforms(transforms: FileTransform[], filePath: string) {
68  return transforms.filter(
69    ({ paths }) =>
70      !paths || arrayize(paths).some((pattern) => minimatch(filePath, pattern, { matchBase: true }))
71  );
72}
73
74async function maybePrintDebugInfoAsync(
75  contentBefore: string,
76  contentAfter: string,
77  filePath: string,
78  transform: FileTransform
79): Promise<void> {
80  if (!transform.debug || contentAfter === contentBefore) {
81    return;
82  }
83  const transformName =
84    typeof transform.debug === 'string' ? transform.debug : JSON.stringify(transform, null, 2);
85
86  printDiff(contentBefore, contentAfter);
87
88  const { isCorrect } = await inquirer.prompt<{ isCorrect: boolean }>([
89    {
90      type: 'confirm',
91      name: 'isCorrect',
92      message: `Changes in file ${filePath} introduced by transform ${transformName}`,
93      default: true,
94    },
95  ]);
96  if (!isCorrect) {
97    throw new Error('ABORTING');
98  }
99}
100
101function applySingleTransform(input: string, transform: StringTransform): string {
102  if (isRawTransform(transform)) {
103    return transform.transform(input);
104  } else if (isReplaceTransform(transform)) {
105    const { find, replaceWith } = transform;
106    // @ts-ignore @tsapeta: TS gets crazy on `replaceWith` being a function.
107    return input.replace(find, replaceWith);
108  }
109  throw new Error(`Unknown transform type`);
110}
111
112/**
113 * Transforms file's content in-place.
114 */
115export async function transformFileAsync(
116  filePath: string,
117  transforms: StringTransform[] | null | undefined
118): Promise<void> {
119  const content = await fs.readFile(filePath, 'utf8');
120  await fs.outputFile(filePath, transformString(content, transforms));
121}
122
123/**
124 * Transforms multiple files' content in-place.
125 */
126export async function transformFilesAsync(
127  files: string[],
128  transforms: FileTransform[]
129): Promise<Set<FileTransform>> {
130  const transformsUsed = new Set<FileTransform>();
131  for (const file of files) {
132    const filteredContentTransforms = getFilteredContentTransforms(transforms ?? [], file);
133
134    // Transform source content.
135    const transformedFile = await applyTransformsOnFileContentAsync(
136      file,
137      filteredContentTransforms
138    );
139    transformedFile.transformsUsed.forEach((transform) => transformsUsed.add(transform));
140
141    // Save transformed content
142    await fs.outputFile(file, transformedFile.content);
143  }
144  return transformsUsed;
145}
146
147/**
148 * Copies a file from source directory to target directory with transformed relative path and content.
149 */
150export async function copyFileWithTransformsAsync(
151  options: CopyFileOptions
152): Promise<CopyFileResult> {
153  const { sourceFile, sourceDirectory, targetDirectory, transforms } = options;
154  const sourcePath = path.join(sourceDirectory, sourceFile);
155
156  // Transform the target path according to rename rules.
157  const targetFile = transformString(sourceFile, transforms.path);
158  const targetPath = path.join(targetDirectory, targetFile);
159
160  const filteredContentTransforms = getFilteredContentTransforms(
161    transforms.content ?? [],
162    sourceFile
163  );
164
165  // Transform source content.
166  const { content, transformsUsed } = await applyTransformsOnFileContentAsync(
167    sourcePath,
168    filteredContentTransforms
169  );
170
171  if (filteredContentTransforms.length > 0) {
172    // Save transformed source file at renamed target path.
173    await fs.outputFile(targetPath, content);
174  } else {
175    // When there are no transforms, it's safer to just copy the file.
176    // Only then binary files will be copied properly since the `content` is utf8-encoded.
177    await fs.copy(sourcePath, targetPath, { overwrite: true });
178  }
179
180  // Keep original file mode if needed.
181  if (options.keepFileMode) {
182    const { mode } = await fs.stat(sourcePath);
183    await fs.chmod(targetPath, mode);
184  }
185
186  return {
187    content,
188    transformsUsed,
189    targetFile,
190  };
191}
192