react-native-bootsplash 5.5.2 → 6.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/generate.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import murmurhash from "@emotion/hash";
2
- import { IOSConfig } from "@expo/config-plugins";
2
+ import { getConfig as getExpoConfig } from "@expo/config";
3
+ import * as Expo from "@expo/config-plugins";
4
+ import { assignColorValue } from "@expo/config-plugins/build/android/Colors";
5
+ import { addImports } from "@expo/config-plugins/build/android/codeMod";
6
+ import { mergeContents } from "@expo/config-plugins/build/utils/generateCode";
3
7
  import plist from "@expo/plist";
4
8
  import { findProjectRoot } from "@react-native-community/cli-tools";
5
9
  import {
@@ -7,7 +11,7 @@ import {
7
11
  IOSProjectConfig,
8
12
  } from "@react-native-community/cli-types";
9
13
  import detectIndent from "detect-indent";
10
- import fs from "fs";
14
+ import fs from "fs-extra";
11
15
  import { parse as parseHtml } from "node-html-parser";
12
16
  import path from "path";
13
17
  import pc from "picocolors";
@@ -21,14 +25,39 @@ import formatXml, { XMLFormatterOptions } from "xml-formatter";
21
25
  import { Manifest } from ".";
22
26
 
23
27
  const workingPath = process.env.INIT_CWD ?? process.env.PWD ?? process.cwd();
28
+ const projectRoot = findProjectRoot(workingPath);
29
+
30
+ export type Platforms = ("android" | "ios" | "web")[];
31
+
32
+ export type RGBColor = {
33
+ R: string;
34
+ G: string;
35
+ B: string;
36
+ };
24
37
 
25
38
  export type Color = {
26
39
  hex: string;
27
- rgb: {
28
- R: string;
29
- G: string;
30
- B: string;
31
- };
40
+ rgb: RGBColor;
41
+ };
42
+
43
+ export const log = {
44
+ error: (text: string) => {
45
+ console.log(pc.red(`❌ ${text}`));
46
+ },
47
+ title: (emoji: string, text: string) => {
48
+ console.log(`\n${emoji} ${pc.underline(pc.bold(text))}`);
49
+ },
50
+ warn: (text: string) => {
51
+ console.log(pc.yellow(`⚠️ ${text}`));
52
+ },
53
+ write: (filePath: string, dimensions?: { width: number; height: number }) => {
54
+ console.log(
55
+ ` ${path.relative(workingPath, filePath)}` +
56
+ (dimensions != null
57
+ ? ` (${dimensions.width}x${dimensions.height})`
58
+ : ""),
59
+ );
60
+ },
32
61
  };
33
62
 
34
63
  export const parseColor = (value: string): Color => {
@@ -45,22 +74,24 @@ export const parseColor = (value: string): Color => {
45
74
  : "#" + up;
46
75
 
47
76
  const rgb: Color["rgb"] = {
48
- R: (parseInt("" + hex[1] + hex[2], 16) / 255).toPrecision(15),
49
- G: (parseInt("" + hex[3] + hex[4], 16) / 255).toPrecision(15),
50
- B: (parseInt("" + hex[5] + hex[6], 16) / 255).toPrecision(15),
77
+ R: (Number.parseInt("" + hex[1] + hex[2], 16) / 255).toPrecision(15),
78
+ G: (Number.parseInt("" + hex[3] + hex[4], 16) / 255).toPrecision(15),
79
+ B: (Number.parseInt("" + hex[5] + hex[6], 16) / 255).toPrecision(15),
51
80
  };
52
81
 
53
- return { hex, rgb };
82
+ return { hex: hex.toLowerCase(), rgb };
54
83
  };
55
84
 
56
85
  const getStoryboard = ({
57
86
  logoHeight,
58
87
  logoWidth,
59
88
  background: { R, G, B },
89
+ fileNameSuffix,
60
90
  }: {
61
91
  logoHeight: number;
62
92
  logoWidth: number;
63
93
  background: Color["rgb"];
94
+ fileNameSuffix: string;
64
95
  }) => {
65
96
  const frameWidth = 375;
66
97
  const frameHeight = 667;
@@ -74,6 +105,7 @@ const getStoryboard = ({
74
105
  <dependencies>
75
106
  <deployment identifier="iOS"/>
76
107
  <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
108
+ <capability name="Named colors" minToolsVersion="9.0"/>
77
109
  <capability name="Safe area layout guides" minToolsVersion="9.0"/>
78
110
  <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
79
111
  </dependencies>
@@ -86,7 +118,7 @@ const getStoryboard = ({
86
118
  <rect key="frame" x="0.0" y="0.0" width="${frameWidth}" height="${frameHeight}"/>
87
119
  <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
88
120
  <subviews>
89
- <imageView autoresizesSubviews="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="BootSplashLogo" translatesAutoresizingMaskIntoConstraints="NO" id="3lX-Ut-9ad">
121
+ <imageView autoresizesSubviews="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="BootSplashLogo-${fileNameSuffix}" translatesAutoresizingMaskIntoConstraints="NO" id="3lX-Ut-9ad">
90
122
  <rect key="frame" x="${logoX}" y="${logoY}" width="${logoWidth}" height="${logoHeight}"/>
91
123
  <accessibility key="accessibilityConfiguration">
92
124
  <accessibilityTraits key="traits" image="YES" notEnabled="YES"/>
@@ -94,7 +126,7 @@ const getStoryboard = ({
94
126
  </imageView>
95
127
  </subviews>
96
128
  <viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
97
- <color key="backgroundColor" red="${R}" green="${G}" blue="${B}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
129
+ <color key="backgroundColor" name="BootSplashBackground-${fileNameSuffix}"/>
98
130
  <constraints>
99
131
  <constraint firstItem="3lX-Ut-9ad" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="Fh9-Fy-1nT"/>
100
132
  <constraint firstItem="3lX-Ut-9ad" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="nvB-Ic-PnI"/>
@@ -107,73 +139,54 @@ const getStoryboard = ({
107
139
  </scene>
108
140
  </scenes>
109
141
  <resources>
110
- <image name="BootSplashLogo" width="${logoWidth}" height="${logoHeight}"/>
142
+ <image name="BootSplashLogo-${fileNameSuffix}" width="${logoWidth}" height="${logoHeight}"/>
143
+ <namedColor name="BootSplashBackground-${fileNameSuffix}">
144
+ <color red="${R}" green="${G}" blue="${B}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
145
+ </namedColor>
111
146
  </resources>
112
147
  </document>
113
148
  `;
114
149
  };
115
150
 
116
- export const addFileToXcodeProject = (filePath: string) => {
117
- const projectRoot = findProjectRoot(workingPath);
118
-
119
- const pbxprojectPath = IOSConfig.Paths.getPBXProjectPath(projectRoot);
120
- const project = IOSConfig.XcodeUtils.getPbxproj(projectRoot);
121
- const xcodeProjectPath = IOSConfig.Paths.getXcodeProjectPath(projectRoot);
122
-
123
- IOSConfig.XcodeUtils.addResourceFileToGroup({
124
- filepath: filePath,
125
- groupName: path.parse(xcodeProjectPath).name,
126
- project,
127
- isBuildFile: true,
128
- });
129
-
130
- hfs.write(pbxprojectPath, project.writeSync());
131
- logWrite(pbxprojectPath);
132
- };
133
-
134
151
  // Freely inspired by https://github.com/humanwhocodes/humanfs
135
152
  export const hfs = {
136
153
  buffer: (path: string) => fs.readFileSync(path),
137
154
  exists: (path: string) => fs.existsSync(path),
155
+ isDir: (path: string) => fs.lstatSync(path).isDirectory(),
138
156
  json: (path: string) => JSON.parse(fs.readFileSync(path, "utf-8")) as unknown,
139
157
  readDir: (path: string) => fs.readdirSync(path, "utf-8"),
140
158
  realPath: (path: string) => fs.realpathSync(path, "utf-8"),
141
- rm: (path: string) => fs.rmSync(path, { force: true }),
159
+ rm: (path: string) => fs.rmSync(path, { force: true, recursive: true }),
142
160
  text: (path: string) => fs.readFileSync(path, "utf-8"),
143
161
 
162
+ copy: (src: string, dest: string) => {
163
+ if (hfs.isDir(src) || !hfs.exists(dest)) {
164
+ return fs.copySync(src, dest, { overwrite: true });
165
+ }
166
+
167
+ const srcBuffer = fs.readFileSync(src);
168
+ const destBuffer = fs.readFileSync(dest);
169
+
170
+ if (!srcBuffer.equals(destBuffer)) {
171
+ return fs.copySync(src, dest, { overwrite: true });
172
+ }
173
+ },
144
174
  ensureDir: (dir: string) => {
145
175
  fs.mkdirSync(dir, { recursive: true });
146
176
  },
147
- write: (file: string, data: string) => {
148
- const trimmed = data.trim();
149
- fs.writeFileSync(file, trimmed === "" ? trimmed : trimmed + "\n", "utf-8");
177
+ write: (path: string, content: string) => {
178
+ const trimmed = content.trim();
179
+ fs.writeFileSync(path, trimmed === "" ? trimmed : trimmed + "\n", "utf-8");
150
180
  },
151
181
  };
152
182
 
153
- export const log = {
154
- error: (text: string) => console.log(pc.red(`❌ ${text}`)),
155
- text: (text: string) => console.log(text),
156
- title: (emoji: string, text: string) =>
157
- console.log(`\n${emoji} ${pc.underline(pc.bold(text))}`),
158
- warn: (text: string) => console.log(pc.yellow(`⚠️ ${text}`)),
159
- };
160
-
161
- export const logWrite = (
162
- filePath: string,
163
- dimensions?: { width: number; height: number },
164
- ) =>
165
- console.log(
166
- ` ${path.relative(workingPath, filePath)}` +
167
- (dimensions != null ? ` (${dimensions.width}x${dimensions.height})` : ""),
168
- );
169
-
170
- export const writeJson = (file: string, json: object) => {
171
- hfs.write(file, JSON.stringify(json, null, 2));
172
- logWrite(file);
183
+ export const writeJson = (filePath: string, content: object) => {
184
+ hfs.write(filePath, JSON.stringify(content, null, 2));
185
+ log.write(filePath);
173
186
  };
174
187
 
175
- export const readXml = (file: string) => {
176
- const xml = hfs.text(file);
188
+ export const readXml = (filePath: string) => {
189
+ const xml = hfs.text(filePath);
177
190
  const { indent } = detectIndent(xml);
178
191
 
179
192
  const formatOptions: XMLFormatterOptions = {
@@ -184,11 +197,11 @@ export const readXml = (file: string) => {
184
197
  };
185
198
 
186
199
  export const writeXml = (
187
- file: string,
188
- xml: string,
200
+ filePath: string,
201
+ content: string,
189
202
  options?: XMLFormatterOptions,
190
203
  ) => {
191
- const formatted = formatXml(xml, {
204
+ const formatted = formatXml(content, {
192
205
  collapseContent: true,
193
206
  forceSelfClosingEmptyTag: true,
194
207
  indentation: " ",
@@ -197,12 +210,12 @@ export const writeXml = (
197
210
  ...options,
198
211
  });
199
212
 
200
- hfs.write(file, formatted);
201
- logWrite(file);
213
+ hfs.write(filePath, formatted);
214
+ log.write(filePath);
202
215
  };
203
216
 
204
- export const readHtml = (file: string) => {
205
- const html = hfs.text(file);
217
+ export const readHtml = (filePath: string) => {
218
+ const html = hfs.text(filePath);
206
219
  const { type, amount } = detectIndent(html);
207
220
 
208
221
  const formatOptions: PrettierOptions = {
@@ -214,11 +227,11 @@ export const readHtml = (file: string) => {
214
227
  };
215
228
 
216
229
  export const writeHtml = async (
217
- file: string,
218
- html: string,
230
+ filePath: string,
231
+ content: string,
219
232
  options?: Omit<PrettierOptions, "parser" | "plugins">,
220
233
  ) => {
221
- const formatted = await prettier.format(html, {
234
+ const formatted = await prettier.format(content, {
222
235
  parser: "html",
223
236
  plugins: [htmlPlugin, cssPlugin],
224
237
  tabWidth: 2,
@@ -226,35 +239,84 @@ export const writeHtml = async (
226
239
  ...options,
227
240
  });
228
241
 
229
- hfs.write(file, formatted);
230
- logWrite(file);
242
+ hfs.write(filePath, formatted);
243
+ log.write(filePath);
231
244
  };
232
245
 
233
- export const cleanIOSAssets = (dir: string, prefix: string) => {
246
+ const cleanIOS = (dir: string) => {
234
247
  hfs
235
248
  .readDir(dir)
236
- .filter((file) => file.startsWith(prefix) && file.endsWith(".png"))
249
+ .filter((file) => file === "Colors.xcassets" || file === "Images.xcassets")
237
250
  .map((file) => path.join(dir, file))
238
- .forEach((file) => hfs.rm(file));
251
+ .flatMap((dir) =>
252
+ hfs
253
+ .readDir(dir)
254
+ .filter((file) => file.startsWith("BootSplash"))
255
+ .map((file) => path.join(dir, file)),
256
+ )
257
+ .forEach((file) => {
258
+ hfs.rm(file);
259
+ });
239
260
  };
240
261
 
241
- export const getIOSAssetFileName = async ({
242
- name,
243
- image,
244
- width,
245
- }: {
246
- name: string;
247
- image: Sharp;
248
- width: number;
249
- }) => {
262
+ const getImageBase64 = async (
263
+ image: Sharp | undefined,
264
+ width: number,
265
+ ): Promise<string> => {
266
+ if (image == null) {
267
+ return "";
268
+ }
269
+
250
270
  const buffer = await image
251
271
  .clone()
252
272
  .resize(width)
253
273
  .png({ quality: 100 })
254
274
  .toBuffer();
255
275
 
256
- const hash = murmurhash(buffer.toString("base64"));
257
- return `${name}-${hash}`;
276
+ return buffer.toString("base64");
277
+ };
278
+
279
+ const getFileNameSuffix = async ({
280
+ background,
281
+ brand,
282
+ brandWidth,
283
+ darkBackground,
284
+ darkBrand,
285
+ darkLogo,
286
+ logo,
287
+ logoWidth,
288
+ }: {
289
+ background: Color;
290
+ brand: Sharp | undefined;
291
+ brandWidth: number;
292
+ darkBackground: Color | undefined;
293
+ darkBrand: Sharp | undefined;
294
+ darkLogo: Sharp | undefined;
295
+ logo: Sharp;
296
+ logoWidth: number;
297
+ }) => {
298
+ const [logoHash, darkLogoHash, brandHash, darkBrandHash] = await Promise.all([
299
+ getImageBase64(logo, logoWidth),
300
+ getImageBase64(darkLogo, logoWidth),
301
+ getImageBase64(brand, brandWidth),
302
+ getImageBase64(darkBrand, brandWidth),
303
+ ]);
304
+
305
+ const record: Record<string, string> = {
306
+ background: background.hex,
307
+ darkBackground: darkBackground?.hex ?? "",
308
+ logo: logoHash,
309
+ darkLogo: darkLogoHash,
310
+ brand: brandHash,
311
+ darkBrand: darkBrandHash,
312
+ };
313
+
314
+ const stableKey = Object.keys(record)
315
+ .sort()
316
+ .map((key) => record[key])
317
+ .join();
318
+
319
+ return murmurhash(stableKey);
258
320
  };
259
321
 
260
322
  const ensureSupportedFormat = async (
@@ -273,23 +335,38 @@ const ensureSupportedFormat = async (
273
335
  }
274
336
  };
275
337
 
276
- const getAndroidResPath = (
277
- android: AndroidProjectConfig,
278
- {
279
- brandHeight,
280
- brandWidth,
281
- flavor,
282
- logoHeight,
283
- logoWidth,
284
- }: {
285
- brandHeight: number;
286
- brandWidth: number;
287
- flavor: string;
288
- logoHeight: number;
289
- logoWidth: number;
290
- },
291
- ): string | undefined => {
292
- const androidResPath = path.resolve(
338
+ const getAndroidOutputPath = ({
339
+ android,
340
+ assetsOutputPath,
341
+ brandHeight,
342
+ brandWidth,
343
+ flavor,
344
+ isExpo,
345
+ logoHeight,
346
+ logoWidth,
347
+ platforms,
348
+ }: {
349
+ android: AndroidProjectConfig | undefined;
350
+ assetsOutputPath: string;
351
+ brandHeight: number;
352
+ brandWidth: number;
353
+ flavor: string;
354
+ isExpo: boolean;
355
+ logoHeight: number;
356
+ logoWidth: number;
357
+ platforms: Platforms;
358
+ }) => {
359
+ if (!platforms.includes("android")) {
360
+ return;
361
+ }
362
+ if (isExpo) {
363
+ return path.resolve(assetsOutputPath, "android");
364
+ }
365
+ if (android == null) {
366
+ return;
367
+ }
368
+
369
+ const androidOutputPath = path.resolve(
293
370
  android.sourceDir,
294
371
  android.appName,
295
372
  "src",
@@ -297,74 +374,123 @@ const getAndroidResPath = (
297
374
  "res",
298
375
  );
299
376
 
300
- if (!hfs.exists(androidResPath)) {
301
- log.warn(
377
+ if (!hfs.exists(androidOutputPath)) {
378
+ return log.warn(
302
379
  `No ${path.relative(
303
380
  workingPath,
304
- androidResPath,
381
+ androidOutputPath,
305
382
  )} directory found. Skipping Android assets generation…`,
306
383
  );
307
- } else if (logoWidth > 288 || logoHeight > 288) {
308
- log.warn(
384
+ }
385
+
386
+ if (logoWidth > 288 || logoHeight > 288) {
387
+ return log.warn(
309
388
  "Logo size exceeding 288x288dp will be cropped by Android. Skipping Android assets generation…",
310
389
  );
311
- } else if (brandHeight > 80 || brandWidth > 200) {
312
- log.warn(
390
+ }
391
+ if (brandWidth > 200 || brandHeight > 80) {
392
+ return log.warn(
313
393
  "Brand size exceeding 200x80dp will be cropped by Android. Skipping Android assets generation…",
314
394
  );
315
- } else {
316
- if (logoWidth > 192 || logoHeight > 192) {
317
- log.warn(`Logo size exceeds 192x192dp. It might be cropped by Android.`);
318
- }
395
+ }
319
396
 
320
- return androidResPath;
397
+ if (logoWidth > 192 || logoHeight > 192) {
398
+ log.warn("Logo size exceeds 192x192dp. It might be cropped by Android.");
321
399
  }
400
+
401
+ return androidOutputPath;
322
402
  };
323
403
 
324
- const getIOSProjectPath = (ios: IOSProjectConfig): string | undefined => {
325
- if (ios.xcodeProject == null) {
326
- log.warn("No Xcode project found. Skipping iOS assets generation…");
404
+ const getIOSOutputPath = ({
405
+ assetsOutputPath,
406
+ ios,
407
+ isExpo,
408
+ platforms,
409
+ }: {
410
+ ios: IOSProjectConfig | undefined;
411
+ assetsOutputPath: string;
412
+ isExpo: boolean;
413
+ platforms: Platforms;
414
+ }) => {
415
+ if (!platforms.includes("ios")) {
416
+ return;
417
+ }
418
+ if (isExpo) {
419
+ return path.resolve(assetsOutputPath, "ios");
420
+ }
421
+ if (ios == null) {
327
422
  return;
328
423
  }
424
+ if (ios.xcodeProject == null) {
425
+ return log.warn("No Xcode project found. Skipping iOS assets generation…");
426
+ }
329
427
 
330
- const iosProjectPath = path
428
+ const iosOutputPath = path
331
429
  .resolve(ios.sourceDir, ios.xcodeProject.name)
332
430
  .replace(/\.(xcodeproj|xcworkspace)$/, "");
333
431
 
334
- if (!hfs.exists(iosProjectPath)) {
335
- log.warn(
432
+ if (!hfs.exists(iosOutputPath)) {
433
+ return log.warn(
336
434
  `No ${path.relative(
337
435
  workingPath,
338
- iosProjectPath,
436
+ iosOutputPath,
339
437
  )} directory found. Skipping iOS assets generation…`,
340
438
  );
341
- } else {
342
- return iosProjectPath;
343
439
  }
440
+
441
+ return iosOutputPath;
344
442
  };
345
443
 
346
- const getHtmlTemplatePath = (html: string): string | undefined => {
444
+ const getHtmlTemplatePath = ({
445
+ html,
446
+ platforms,
447
+ }: {
448
+ html: string;
449
+ platforms: Platforms;
450
+ }) => {
451
+ if (!platforms.includes("web")) {
452
+ return;
453
+ }
454
+
347
455
  const htmlTemplatePath = path.resolve(workingPath, html);
348
456
 
349
457
  if (!hfs.exists(htmlTemplatePath)) {
350
- log.warn(
458
+ return log.warn(
351
459
  `No ${path.relative(
352
460
  workingPath,
353
461
  htmlTemplatePath,
354
462
  )} file found. Skipping HTML + CSS generation…`,
355
463
  );
356
- } else {
357
- return htmlTemplatePath;
358
464
  }
465
+
466
+ return htmlTemplatePath;
467
+ };
468
+
469
+ export const getImageHeight = (
470
+ image: Sharp | undefined,
471
+ width: number,
472
+ ): Promise<number> => {
473
+ if (image == null) {
474
+ return Promise.resolve(0);
475
+ }
476
+
477
+ return image
478
+ .clone()
479
+ .resize(width)
480
+ .toBuffer()
481
+ .then((buffer) => sharp(buffer).metadata())
482
+ .then(({ height = 0 }) => Math.round(height));
359
483
  };
360
484
 
361
485
  export type AddonConfig = {
362
486
  licenseKey: string;
487
+ isExpo: boolean;
488
+ fileNameSuffix: string;
363
489
 
364
- androidResPath: string | undefined;
365
- iosProjectPath: string | undefined;
366
- htmlTemplatePath: string | undefined;
367
- assetsOutputPath: string | undefined;
490
+ androidOutputPath: string | void;
491
+ iosOutputPath: string | void;
492
+ htmlTemplatePath: string | void;
493
+ assetsOutputPath: string;
368
494
 
369
495
  logoPath: string;
370
496
  darkLogoPath: string | undefined;
@@ -408,10 +534,10 @@ export const generate = async ({
408
534
  ios?: IOSProjectConfig;
409
535
 
410
536
  logo: string;
411
- platforms: string[];
537
+ platforms: Platforms;
412
538
  background: string;
413
539
  logoWidth: number;
414
- assetsOutput?: string;
540
+ assetsOutput: string;
415
541
  html: string;
416
542
  flavor: string;
417
543
 
@@ -422,10 +548,14 @@ export const generate = async ({
422
548
  darkLogo?: string;
423
549
  darkBrand?: string;
424
550
  }) => {
551
+ const isExpo =
552
+ getExpoConfig(projectRoot, { skipSDKVersionRequirement: true }).exp
553
+ .sdkVersion != null;
554
+
425
555
  const [nodeStringVersion = ""] = process.versions.node.split(".");
426
- const nodeVersion = parseInt(nodeStringVersion, 10);
556
+ const nodeVersion = Number.parseInt(nodeStringVersion, 10);
427
557
 
428
- if (!isNaN(nodeVersion) && nodeVersion < 18) {
558
+ if (!Number.isNaN(nodeVersion) && nodeVersion < 18) {
429
559
  log.error("Requires Node 18 (or higher)");
430
560
  process.exit(1);
431
561
  }
@@ -445,10 +575,7 @@ export const generate = async ({
445
575
  ? path.resolve(workingPath, args.darkBrand)
446
576
  : undefined;
447
577
 
448
- const assetsOutputPath =
449
- args.assetsOutput != null
450
- ? path.resolve(workingPath, args.assetsOutput)
451
- : undefined;
578
+ const assetsOutputPath = path.resolve(workingPath, args.assetsOutput);
452
579
 
453
580
  const logo = sharp(logoPath);
454
581
  const darkLogo = darkLogoPath != null ? sharp(darkLogoPath) : undefined;
@@ -470,7 +597,7 @@ export const generate = async ({
470
597
 
471
598
  if (licenseKey != null && !executeAddon) {
472
599
  log.warn(
473
- `You specified a license key but none of the options that requires it.`,
600
+ "You specified a license key but none of the options that requires it.",
474
601
  );
475
602
  }
476
603
 
@@ -499,20 +626,8 @@ export const generate = async ({
499
626
  await ensureSupportedFormat("Brand", brand);
500
627
  await ensureSupportedFormat("Dark brand", darkBrand);
501
628
 
502
- const logoHeight = await logo
503
- .clone()
504
- .resize(logoWidth)
505
- .toBuffer()
506
- .then((buffer) => sharp(buffer).metadata())
507
- .then(({ height = 0 }) => Math.round(height));
508
-
509
- const brandHeight =
510
- (await brand
511
- ?.clone()
512
- .resize(brandWidth)
513
- .toBuffer()
514
- .then((buffer) => sharp(buffer).metadata())
515
- .then(({ height = 0 }) => Math.round(height))) ?? 0;
629
+ const logoHeight = await getImageHeight(logo, logoWidth);
630
+ const brandHeight = await getImageHeight(brand, brandWidth);
516
631
 
517
632
  if (logoWidth < args.logoWidth) {
518
633
  log.warn(
@@ -525,52 +640,45 @@ export const generate = async ({
525
640
  );
526
641
  }
527
642
 
528
- const androidResPath =
529
- platforms.includes("android") && android != null
530
- ? getAndroidResPath(android, {
531
- brandHeight,
532
- brandWidth,
533
- flavor,
534
- logoHeight,
535
- logoWidth,
536
- })
537
- : undefined;
538
-
539
- const iosProjectPath =
540
- platforms.includes("ios") && ios != null
541
- ? getIOSProjectPath(ios)
542
- : undefined;
543
-
544
- const htmlTemplatePath = platforms.includes("web")
545
- ? getHtmlTemplatePath(html)
546
- : undefined;
547
-
548
- if (androidResPath != null) {
549
- log.title("🤖", "Android");
643
+ const fileNameSuffix = await getFileNameSuffix({
644
+ background,
645
+ brand,
646
+ brandWidth,
647
+ darkBackground,
648
+ darkBrand,
649
+ darkLogo,
650
+ logo,
651
+ logoWidth,
652
+ });
550
653
 
551
- const valuesPath = path.resolve(androidResPath, "values");
552
- hfs.ensureDir(valuesPath);
654
+ const androidOutputPath = getAndroidOutputPath({
655
+ android,
656
+ assetsOutputPath,
657
+ brandHeight,
658
+ brandWidth,
659
+ flavor,
660
+ isExpo,
661
+ logoHeight,
662
+ logoWidth,
663
+ platforms,
664
+ });
553
665
 
554
- const colorsXmlPath = path.resolve(valuesPath, "colors.xml");
555
- const colorsXmlEntry = `<color name="bootsplash_background">${background.hex}</color>`;
666
+ const iosOutputPath = getIOSOutputPath({
667
+ assetsOutputPath,
668
+ ios,
669
+ isExpo,
670
+ platforms,
671
+ });
556
672
 
557
- if (hfs.exists(colorsXmlPath)) {
558
- const { root, formatOptions } = readXml(colorsXmlPath);
559
- const nextColor = parseHtml(colorsXmlEntry);
560
- const prevColor = root.querySelector(
561
- 'color[name="bootsplash_background"]',
562
- );
673
+ const htmlTemplatePath = getHtmlTemplatePath({
674
+ html,
675
+ platforms,
676
+ });
563
677
 
564
- if (prevColor != null) {
565
- prevColor.replaceWith(nextColor);
566
- } else {
567
- root.querySelector("resources")?.appendChild(nextColor);
568
- }
678
+ if (androidOutputPath != null) {
679
+ log.title("🤖", "Android");
569
680
 
570
- writeXml(colorsXmlPath, root.toString(), formatOptions);
571
- } else {
572
- writeXml(colorsXmlPath, `<resources>${colorsXmlEntry}</resources>`);
573
- }
681
+ hfs.ensureDir(androidOutputPath);
574
682
 
575
683
  await Promise.all(
576
684
  [
@@ -581,7 +689,7 @@ export const generate = async ({
581
689
  { ratio: 4, suffix: "xxxhdpi" },
582
690
  ].map(({ ratio, suffix }) => {
583
691
  const drawableDirPath = path.resolve(
584
- androidResPath,
692
+ androidOutputPath,
585
693
  `drawable-${suffix}`,
586
694
  );
587
695
 
@@ -618,74 +726,99 @@ export const generate = async ({
618
726
  .toFile(filePath),
619
727
  )
620
728
  .then(() => {
621
- logWrite(filePath, {
729
+ log.write(filePath, {
622
730
  width: canvasSize,
623
731
  height: canvasSize,
624
732
  });
625
733
  });
626
734
  }),
627
735
  );
628
- }
629
736
 
630
- if (iosProjectPath != null) {
631
- log.title("🍏", "iOS");
737
+ if (!isExpo) {
738
+ const valuesPath = path.resolve(androidOutputPath, "values");
739
+ hfs.ensureDir(valuesPath);
632
740
 
633
- const storyboardPath = path.resolve(
634
- iosProjectPath,
635
- "BootSplash.storyboard",
636
- );
741
+ const colorsXmlPath = path.resolve(valuesPath, "colors.xml");
742
+ const colorsXmlEntry = `<color name="bootsplash_background">${background.hex}</color>`;
637
743
 
638
- writeXml(
639
- storyboardPath,
640
- getStoryboard({
641
- logoHeight,
642
- logoWidth,
643
- background: background.rgb,
644
- }),
645
- { whiteSpaceAtEndOfSelfclosingTag: false },
646
- );
744
+ if (hfs.exists(colorsXmlPath)) {
745
+ const { root, formatOptions } = readXml(colorsXmlPath);
746
+ const nextColor = parseHtml(colorsXmlEntry);
647
747
 
648
- addFileToXcodeProject(
649
- path.join(path.basename(iosProjectPath), "BootSplash.storyboard"),
650
- );
748
+ const prevColor = root.querySelector(
749
+ 'color[name="bootsplash_background"]',
750
+ );
751
+
752
+ if (prevColor != null) {
753
+ prevColor.replaceWith(nextColor);
754
+ } else {
755
+ root.querySelector("resources")?.appendChild(nextColor);
756
+ }
651
757
 
652
- const infoPlistPath = path.join(iosProjectPath, "Info.plist");
758
+ writeXml(colorsXmlPath, root.toString(), formatOptions);
759
+ } else {
760
+ writeXml(colorsXmlPath, `<resources>${colorsXmlEntry}</resources>`);
761
+ }
762
+ }
763
+ }
653
764
 
654
- const infoPlist = plist.parse(hfs.text(infoPlistPath)) as Record<
655
- string,
656
- unknown
657
- >;
765
+ if (iosOutputPath != null) {
766
+ log.title("🍏", "iOS");
658
767
 
659
- infoPlist["UILaunchStoryboardName"] = "BootSplash.storyboard";
768
+ hfs.ensureDir(iosOutputPath);
769
+ cleanIOS(iosOutputPath);
660
770
 
661
- const formatted = formatXml(plist.build(infoPlist), {
662
- collapseContent: true,
663
- forceSelfClosingEmptyTag: false,
664
- indentation: "\t",
665
- lineSeparator: "\n",
666
- whiteSpaceAtEndOfSelfclosingTag: false,
667
- })
668
- .replace(/<string\/>/gm, "<string></string>")
669
- .replace(/^\t/gm, "");
771
+ const storyboardPath = path.resolve(iosOutputPath, "BootSplash.storyboard");
670
772
 
671
- hfs.write(infoPlistPath, formatted);
672
- logWrite(infoPlistPath);
773
+ const colorsSetPath = path.resolve(
774
+ iosOutputPath,
775
+ "Colors.xcassets",
776
+ `BootSplashBackground-${fileNameSuffix}.colorset`,
777
+ );
673
778
 
674
779
  const imageSetPath = path.resolve(
675
- iosProjectPath,
780
+ iosOutputPath,
676
781
  "Images.xcassets",
677
- "BootSplashLogo.imageset",
782
+ `BootSplashLogo-${fileNameSuffix}.imageset`,
678
783
  );
679
784
 
785
+ hfs.ensureDir(colorsSetPath);
680
786
  hfs.ensureDir(imageSetPath);
681
- cleanIOSAssets(imageSetPath, "bootsplash_logo");
682
787
 
683
- const logoFileName = await getIOSAssetFileName({
684
- name: "bootsplash_logo",
685
- image: logo,
686
- width: logoWidth,
788
+ writeXml(
789
+ storyboardPath,
790
+ getStoryboard({
791
+ logoHeight,
792
+ logoWidth,
793
+ background: background.rgb,
794
+ fileNameSuffix,
795
+ }),
796
+ { whiteSpaceAtEndOfSelfclosingTag: false },
797
+ );
798
+
799
+ writeJson(path.resolve(colorsSetPath, "Contents.json"), {
800
+ colors: [
801
+ {
802
+ idiom: "universal",
803
+ color: {
804
+ "color-space": "srgb",
805
+ components: {
806
+ blue: background.rgb.B,
807
+ green: background.rgb.G,
808
+ red: background.rgb.R,
809
+ alpha: "1.000",
810
+ },
811
+ },
812
+ },
813
+ ],
814
+ info: {
815
+ author: "xcode",
816
+ version: 1,
817
+ },
687
818
  });
688
819
 
820
+ const logoFileName = `logo-${fileNameSuffix}`;
821
+
689
822
  writeJson(path.resolve(imageSetPath, "Contents.json"), {
690
823
  images: [
691
824
  {
@@ -727,10 +860,60 @@ export const generate = async ({
727
860
  .png({ quality: 100 })
728
861
  .toFile(filePath)
729
862
  .then(({ width, height }) => {
730
- logWrite(filePath, { width, height });
863
+ log.write(filePath, { width, height });
731
864
  });
732
865
  }),
733
866
  );
867
+
868
+ if (!isExpo) {
869
+ const infoPlistPath = path.resolve(iosOutputPath, "Info.plist");
870
+
871
+ const infoPlist = plist.parse(hfs.text(infoPlistPath)) as Record<
872
+ string,
873
+ unknown
874
+ >;
875
+
876
+ infoPlist["UILaunchStoryboardName"] = "BootSplash";
877
+
878
+ const formatted = formatXml(plist.build(infoPlist), {
879
+ collapseContent: true,
880
+ forceSelfClosingEmptyTag: false,
881
+ indentation: "\t",
882
+ lineSeparator: "\n",
883
+ whiteSpaceAtEndOfSelfclosingTag: false,
884
+ })
885
+ .replace(/<string\/>/gm, "<string></string>")
886
+ .replace(/^\t/gm, "");
887
+
888
+ hfs.write(infoPlistPath, formatted);
889
+ log.write(infoPlistPath);
890
+
891
+ const pbxprojectPath =
892
+ Expo.IOSConfig.Paths.getPBXProjectPath(projectRoot);
893
+
894
+ const xcodeProjectPath =
895
+ Expo.IOSConfig.Paths.getXcodeProjectPath(projectRoot);
896
+
897
+ const project = Expo.IOSConfig.XcodeUtils.getPbxproj(projectRoot);
898
+ const projectName = path.basename(iosOutputPath);
899
+
900
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
901
+ filepath: path.join(projectName, "BootSplash.storyboard"),
902
+ groupName: path.parse(xcodeProjectPath).name,
903
+ project,
904
+ isBuildFile: true,
905
+ });
906
+
907
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
908
+ filepath: path.join(projectName, "Colors.xcassets"),
909
+ groupName: path.parse(xcodeProjectPath).name,
910
+ project,
911
+ isBuildFile: true,
912
+ });
913
+
914
+ hfs.write(pbxprojectPath, project.writeSync());
915
+ log.write(pbxprojectPath);
916
+ }
734
917
  }
735
918
 
736
919
  if (htmlTemplatePath != null) {
@@ -797,52 +980,49 @@ export const generate = async ({
797
980
  await writeHtml(htmlTemplatePath, root.toString(), formatOptions);
798
981
  }
799
982
 
800
- if (assetsOutputPath != null) {
801
- log.title("📄", "Assets");
802
-
803
- hfs.ensureDir(assetsOutputPath);
983
+ log.title("📄", "Assets");
804
984
 
805
- writeJson(path.resolve(assetsOutputPath, "bootsplash_manifest.json"), {
806
- background: background.hex,
807
- logo: {
808
- width: logoWidth,
809
- height: logoHeight,
810
- },
811
- } satisfies Manifest);
812
-
813
- await Promise.all(
814
- [
815
- { ratio: 1, suffix: "" },
816
- { ratio: 1.5, suffix: "@1,5x" },
817
- { ratio: 2, suffix: "@2x" },
818
- { ratio: 3, suffix: "@3x" },
819
- { ratio: 4, suffix: "@4x" },
820
- ].map(({ ratio, suffix }) => {
821
- const filePath = path.resolve(
822
- assetsOutputPath,
823
- `bootsplash_logo${suffix}.png`,
824
- );
985
+ hfs.ensureDir(assetsOutputPath);
825
986
 
826
- return logo
827
- .clone()
828
- .resize(Math.round(logoWidth * ratio))
829
- .png({ quality: 100 })
830
- .toFile(filePath)
831
- .then(({ width, height }) => {
832
- logWrite(filePath, { width, height });
833
- });
834
- }),
835
- );
836
- }
987
+ writeJson(path.resolve(assetsOutputPath, "manifest.json"), {
988
+ background: background.hex,
989
+ logo: {
990
+ width: logoWidth,
991
+ height: logoHeight,
992
+ },
993
+ } satisfies Manifest);
994
+
995
+ await Promise.all(
996
+ [
997
+ { ratio: 1, suffix: "" },
998
+ { ratio: 1.5, suffix: "@1,5x" },
999
+ { ratio: 2, suffix: "@2x" },
1000
+ { ratio: 3, suffix: "@3x" },
1001
+ { ratio: 4, suffix: "@4x" },
1002
+ ].map(({ ratio, suffix }) => {
1003
+ const filePath = path.resolve(assetsOutputPath, `logo${suffix}.png`);
1004
+
1005
+ return logo
1006
+ .clone()
1007
+ .resize(Math.round(logoWidth * ratio))
1008
+ .png({ quality: 100 })
1009
+ .toFile(filePath)
1010
+ .then(({ width, height }) => {
1011
+ log.write(filePath, { width, height });
1012
+ });
1013
+ }),
1014
+ );
837
1015
 
838
1016
  if (licenseKey != null && executeAddon) {
839
1017
  const addon = requireAddon();
840
1018
 
841
1019
  await addon?.execute({
842
1020
  licenseKey,
1021
+ isExpo,
1022
+ fileNameSuffix,
843
1023
 
844
- androidResPath,
845
- iosProjectPath,
1024
+ androidOutputPath,
1025
+ iosOutputPath,
846
1026
  htmlTemplatePath,
847
1027
  assetsOutputPath,
848
1028
 
@@ -865,7 +1045,7 @@ export const generate = async ({
865
1045
  darkBrand,
866
1046
  });
867
1047
  } else {
868
- log.text(`
1048
+ console.log(`
869
1049
  ${pc.blue("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")}
870
1050
  ${pc.blue("┃")} 🔑 ${pc.bold(
871
1051
  "Get a license key for brand image / dark mode support",
@@ -876,5 +1056,325 @@ ${pc.blue("┃")} ${pc.underline(
876
1056
  ${pc.blue("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")}`);
877
1057
  }
878
1058
 
879
- log.text(`\n💖 Thanks for using ${pc.underline("react-native-bootsplash")}`);
1059
+ console.log(
1060
+ `\n💖 Thanks for using ${pc.underline("react-native-bootsplash")}`,
1061
+ );
1062
+ };
1063
+
1064
+ export type ExpoProps = {
1065
+ assetsDir?: string;
1066
+ edgeToEdge?: boolean;
1067
+ };
1068
+
1069
+ export type ExpoPlugin = Expo.ConfigPlugin<ExpoProps>;
1070
+
1071
+ const withAndroidAssets: ExpoPlugin = (config, props) =>
1072
+ Expo.withDangerousMod(config, [
1073
+ "android",
1074
+ (config) => {
1075
+ const { assetsDir = "assets/bootsplash" } = props;
1076
+ const { platformProjectRoot } = config.modRequest;
1077
+
1078
+ const srcDir = path.resolve(workingPath, assetsDir, "android");
1079
+
1080
+ const destDir = path.resolve(
1081
+ platformProjectRoot,
1082
+ "app",
1083
+ "src",
1084
+ "main",
1085
+ "res",
1086
+ );
1087
+
1088
+ for (const drawableDir of hfs.readDir(srcDir)) {
1089
+ const srcDrawableDir = path.join(srcDir, drawableDir);
1090
+ const destDrawableDir = path.join(destDir, drawableDir);
1091
+
1092
+ hfs.ensureDir(destDrawableDir);
1093
+
1094
+ for (const file of hfs.readDir(srcDrawableDir)) {
1095
+ hfs.copy(
1096
+ path.join(srcDrawableDir, file),
1097
+ path.join(destDrawableDir, file),
1098
+ );
1099
+ }
1100
+ }
1101
+
1102
+ return config;
1103
+ },
1104
+ ]);
1105
+
1106
+ const withAndroidManifest: ExpoPlugin = (config) =>
1107
+ Expo.withAndroidManifest(config, (config) => {
1108
+ config.modResults.manifest.application?.forEach((application) => {
1109
+ if (application.$["android:name"] === ".MainApplication") {
1110
+ const { activity } = application;
1111
+
1112
+ activity?.forEach((activity) => {
1113
+ if (activity.$["android:name"] === ".MainActivity") {
1114
+ activity.$["android:theme"] = "@style/BootTheme";
1115
+ }
1116
+ });
1117
+ }
1118
+ });
1119
+
1120
+ return config;
1121
+ });
1122
+
1123
+ const withMainActivity: ExpoPlugin = (config) =>
1124
+ Expo.withMainActivity(config, (config) => {
1125
+ const { modResults } = config;
1126
+ const { language } = modResults;
1127
+
1128
+ const withImports = addImports(
1129
+ modResults.contents.replace(
1130
+ /(\/\/ )?setTheme\(R\.style\.AppTheme\)/,
1131
+ "// setTheme(R.style.AppTheme)",
1132
+ ),
1133
+ ["android.os.Bundle", "com.zoontek.rnbootsplash.RNBootSplash"],
1134
+ language === "java",
1135
+ );
1136
+
1137
+ // indented with 4 spaces
1138
+ const withInit = mergeContents({
1139
+ src: withImports,
1140
+ comment: " //",
1141
+ tag: "bootsplash-init",
1142
+ offset: 0,
1143
+ anchor: /super\.onCreate\(null\)/,
1144
+ newSrc:
1145
+ " RNBootSplash.init(this, R.style.BootTheme)" +
1146
+ (language === "java" ? ";" : ""),
1147
+ });
1148
+
1149
+ return {
1150
+ ...config,
1151
+ modResults: {
1152
+ ...modResults,
1153
+ contents: withInit.contents,
1154
+ },
1155
+ };
1156
+ });
1157
+
1158
+ const withAndroidStyles: ExpoPlugin = (config, props) =>
1159
+ Expo.withAndroidStyles(config, async (config) => {
1160
+ const { assetsDir = "assets/bootsplash", edgeToEdge = false } = props;
1161
+
1162
+ const manifest = (await hfs.json(
1163
+ path.resolve(workingPath, assetsDir, "manifest.json"),
1164
+ )) as Manifest;
1165
+
1166
+ const item = [
1167
+ {
1168
+ $: { name: "postBootSplashTheme" },
1169
+ _: "@style/AppTheme",
1170
+ },
1171
+ {
1172
+ $: { name: "bootSplashBackground" },
1173
+ _: "@color/bootsplash_background",
1174
+ },
1175
+ {
1176
+ $: { name: "bootSplashLogo" },
1177
+ _: "@drawable/bootsplash_logo",
1178
+ },
1179
+ ];
1180
+
1181
+ if (manifest.brand != null) {
1182
+ item.push({
1183
+ $: { name: "bootSplashBrand" },
1184
+ _: "@drawable/bootsplash_brand",
1185
+ });
1186
+ }
1187
+
1188
+ config.modResults.resources.style
1189
+ ?.filter(({ $ }) => $.name !== "BootTheme")
1190
+ .push({
1191
+ item,
1192
+ $: {
1193
+ name: "BootTheme",
1194
+ parent: edgeToEdge
1195
+ ? "Theme.BootSplash"
1196
+ : "Theme.BootSplash.EdgeToEdge",
1197
+ },
1198
+ });
1199
+
1200
+ return config;
1201
+ });
1202
+
1203
+ const withAndroidColors: ExpoPlugin = (config, props) =>
1204
+ Expo.withAndroidColors(config, async (config) => {
1205
+ const { assetsDir = "assets/bootsplash" } = props;
1206
+
1207
+ const manifest = (await hfs.json(
1208
+ path.resolve(workingPath, assetsDir, "manifest.json"),
1209
+ )) as Manifest;
1210
+
1211
+ config.modResults = assignColorValue(config.modResults, {
1212
+ name: "bootsplash_background",
1213
+ value: manifest.background,
1214
+ });
1215
+
1216
+ return config;
1217
+ });
1218
+
1219
+ const withAndroidColorsNight: ExpoPlugin = (config, props) =>
1220
+ Expo.withAndroidColorsNight(config, async (config) => {
1221
+ const { assetsDir = "assets/bootsplash" } = props;
1222
+
1223
+ const manifest = (await hfs.json(
1224
+ path.resolve(workingPath, assetsDir, "manifest.json"),
1225
+ )) as Manifest;
1226
+
1227
+ if (manifest.darkBackground != null) {
1228
+ config.modResults = assignColorValue(config.modResults, {
1229
+ name: "bootsplash_background",
1230
+ value: manifest.darkBackground,
1231
+ });
1232
+ }
1233
+
1234
+ return config;
1235
+ });
1236
+
1237
+ const withIOSAssets: ExpoPlugin = (config, props) =>
1238
+ Expo.withDangerousMod(config, [
1239
+ "ios",
1240
+ (config) => {
1241
+ const { assetsDir = "assets/bootsplash" } = props;
1242
+ const { platformProjectRoot, projectName = "" } = config.modRequest;
1243
+
1244
+ const srcDir = path.resolve(workingPath, assetsDir, "ios");
1245
+ const destDir = path.resolve(platformProjectRoot, projectName);
1246
+
1247
+ cleanIOS(destDir);
1248
+
1249
+ hfs.copy(
1250
+ path.join(srcDir, "BootSplash.storyboard"),
1251
+ path.join(destDir, "BootSplash.storyboard"),
1252
+ );
1253
+
1254
+ for (const xcassetsDir of ["Colors.xcassets", "Images.xcassets"]) {
1255
+ const srcXcassetsDir = path.join(srcDir, xcassetsDir);
1256
+ const destXcassetsDir = path.join(destDir, xcassetsDir);
1257
+
1258
+ hfs.ensureDir(destXcassetsDir);
1259
+
1260
+ for (const file of hfs.readDir(srcXcassetsDir)) {
1261
+ hfs.copy(
1262
+ path.join(srcXcassetsDir, file),
1263
+ path.join(destXcassetsDir, file),
1264
+ );
1265
+ }
1266
+ }
1267
+
1268
+ return config;
1269
+ },
1270
+ ]);
1271
+
1272
+ const withAppDelegate: ExpoPlugin = (config) =>
1273
+ Expo.withAppDelegate(config, (config) => {
1274
+ const [sdkStringVersion = ""] = config.sdkVersion?.split(".") ?? "";
1275
+ const isAtLeastExpo51 = Number.parseInt(sdkStringVersion, 10) >= 51;
1276
+
1277
+ const { modResults } = config;
1278
+ const { language } = modResults;
1279
+
1280
+ if (language !== "objc" && language !== "objcpp") {
1281
+ throw new Error(
1282
+ `Cannot modify the project AppDelegate as it's not in a supported language: ${language}`,
1283
+ );
1284
+ }
1285
+
1286
+ const withHeader = mergeContents({
1287
+ src: modResults.contents,
1288
+ comment: "//",
1289
+ tag: "bootsplash-header",
1290
+ offset: 1,
1291
+ anchor: /#import "AppDelegate\.h"/,
1292
+ newSrc: '#import "RNBootSplash.h"',
1293
+ });
1294
+
1295
+ const withRootView = mergeContents({
1296
+ src: withHeader.contents,
1297
+ comment: "//",
1298
+ tag: "bootsplash-init",
1299
+ offset: 0,
1300
+ anchor: /@end/,
1301
+ newSrc: isAtLeastExpo51
1302
+ ? dedent`
1303
+ - (void)customizeRootView:(RCTRootView *)rootView {
1304
+ [RNBootSplash initWithStoryboard:@"BootSplash" rootView:rootView];
1305
+ }
1306
+ `
1307
+ : dedent`
1308
+ - (UIView *)createRootViewWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName initProps:(NSDictionary *)initProps {
1309
+ UIView *rootView = [super createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];
1310
+ [RNBootSplash initWithStoryboard:@"BootSplash" rootView:rootView];
1311
+ return rootView;
1312
+ }
1313
+ `,
1314
+ });
1315
+
1316
+ return {
1317
+ ...config,
1318
+ modResults: {
1319
+ ...modResults,
1320
+ contents: withRootView.contents,
1321
+ },
1322
+ };
1323
+ });
1324
+
1325
+ const withInfoPlist: ExpoPlugin = (config) =>
1326
+ Expo.withInfoPlist(config, (config) => {
1327
+ config.modResults["UILaunchStoryboardName"] = "BootSplash";
1328
+ return config;
1329
+ });
1330
+
1331
+ const withXcodeProject: ExpoPlugin = (config) =>
1332
+ Expo.withXcodeProject(config, (config) => {
1333
+ const { projectName = "" } = config.modRequest;
1334
+
1335
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
1336
+ filepath: path.join(projectName, "BootSplash.storyboard"),
1337
+ groupName: projectName,
1338
+ project: config.modResults,
1339
+ isBuildFile: true,
1340
+ });
1341
+
1342
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
1343
+ filepath: path.join(projectName, "Colors.xcassets"),
1344
+ groupName: projectName,
1345
+ project: config.modResults,
1346
+ isBuildFile: true,
1347
+ });
1348
+
1349
+ return config;
1350
+ });
1351
+
1352
+ export const withGenerate: ExpoPlugin = (config, props = {}) => {
1353
+ const plugins: ExpoPlugin[] = [];
1354
+ const { platforms = [] } = config;
1355
+
1356
+ if (platforms.includes("android")) {
1357
+ plugins.push(
1358
+ withAndroidAssets,
1359
+ withAndroidManifest,
1360
+ withMainActivity,
1361
+ withAndroidStyles,
1362
+ withAndroidColors,
1363
+ withAndroidColorsNight,
1364
+ );
1365
+ }
1366
+
1367
+ if (platforms.includes("ios")) {
1368
+ plugins.push(
1369
+ withIOSAssets,
1370
+ withAppDelegate,
1371
+ withInfoPlist,
1372
+ withXcodeProject,
1373
+ );
1374
+ }
1375
+
1376
+ return Expo.withPlugins(
1377
+ config,
1378
+ plugins.map((plugin) => [plugin, props]),
1379
+ );
880
1380
  };