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