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