1/**
2 * Get line indexes for the generated section of a file.
3 *
4 * @param src
5 */
6import crypto from 'crypto';
7
8function getGeneratedSectionIndexes(
9  src: string,
10  tag: string
11): { contents: string[]; start: number; end: number } {
12  const contents = src.split('\n');
13  const start = contents.findIndex((line) => line.includes(`@generated begin ${tag}`));
14  const end = contents.findIndex((line) => line.includes(`@generated end ${tag}`));
15
16  return { contents, start, end };
17}
18
19export type MergeResults = {
20  contents: string;
21  didClear: boolean;
22  didMerge: boolean;
23};
24
25/**
26 * Merge the contents of two files together and add a generated header.
27 *
28 * @param src contents of the original file
29 * @param newSrc new contents to merge into the original file
30 * @param identifier used to update and remove merges
31 * @param anchor regex to where the merge should begin
32 * @param offset line offset to start merging at (<1 for behind the anchor)
33 * @param comment comment style `//` or `#`
34 */
35export function mergeContents({
36  src,
37  newSrc,
38  tag,
39  anchor,
40  offset,
41  comment,
42}: {
43  src: string;
44  newSrc: string;
45  tag: string;
46  anchor: string | RegExp;
47  offset: number;
48  comment: string;
49}): MergeResults {
50  const header = createGeneratedHeaderComment(newSrc, tag, comment);
51  if (!src.includes(header)) {
52    // Ensure the old generated contents are removed.
53    const sanitizedTarget = removeGeneratedContents(src, tag);
54    return {
55      contents: addLines(sanitizedTarget ?? src, anchor, offset, [
56        header,
57        ...newSrc.split('\n'),
58        `${comment} @generated end ${tag}`,
59      ]),
60      didMerge: true,
61      didClear: !!sanitizedTarget,
62    };
63  }
64  return { contents: src, didClear: false, didMerge: false };
65}
66
67export function removeContents({ src, tag }: { src: string; tag: string }): MergeResults {
68  // Ensure the old generated contents are removed.
69  const sanitizedTarget = removeGeneratedContents(src, tag);
70  return {
71    contents: sanitizedTarget ?? src,
72    didMerge: false,
73    didClear: !!sanitizedTarget,
74  };
75}
76
77function addLines(content: string, find: string | RegExp, offset: number, toAdd: string[]) {
78  const lines = content.split('\n');
79
80  let lineIndex = lines.findIndex((line) => line.match(find));
81  if (lineIndex < 0) {
82    const error = new Error(`Failed to match "${find}" in contents:\n${content}`);
83    // @ts-ignore
84    error.code = 'ERR_NO_MATCH';
85    throw error;
86  }
87  for (const newLine of toAdd) {
88    lines.splice(lineIndex + offset, 0, newLine);
89    lineIndex++;
90  }
91
92  return lines.join('\n');
93}
94
95/**
96 * Removes the generated section from a file, returns null when nothing can be removed.
97 * This sways heavily towards not removing lines unless it's certain that modifications were not made manually.
98 *
99 * @param src
100 */
101export function removeGeneratedContents(src: string, tag: string): string | null {
102  const { contents, start, end } = getGeneratedSectionIndexes(src, tag);
103  if (start > -1 && end > -1 && start < end) {
104    contents.splice(start, end - start + 1);
105    // TODO: We could in theory check that the contents we're removing match the hash used in the header,
106    // this would ensure that we don't accidentally remove lines that someone added or removed from the generated section.
107    return contents.join('\n');
108  }
109  return null;
110}
111
112export function createGeneratedHeaderComment(
113  contents: string,
114  tag: string,
115  comment: string
116): string {
117  const hashKey = createHash(contents);
118
119  // Everything after the `${tag} ` is unversioned and can be freely modified without breaking changes.
120  return `${comment} @generated begin ${tag} - expo prebuild (DO NOT MODIFY) ${hashKey}`;
121}
122
123export function createHash(src: string): string {
124  // this doesn't need to be secure, the shorter the better.
125  const hash = crypto.createHash('sha1').update(src).digest('hex');
126  return `sync-${hash}`;
127}
128