react-native-bootsplash 6.3.11 → 7.0.0-beta.1

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.
Files changed (75) hide show
  1. package/README.md +13 -41
  2. package/android/build.gradle +10 -17
  3. package/android/src/main/AndroidManifest.xml +1 -1
  4. package/android/src/main/java/com/zoontek/rnbootsplash/RNBootSplashModuleImpl.kt +2 -2
  5. package/android/src/main/java/com/zoontek/rnbootsplash/RNBootSplashPackage.kt +0 -1
  6. package/android/src/main/java/com/zoontek/rnbootsplash/RNBootSplashQueue.kt +1 -1
  7. package/android/src/main/res/drawable/compat_splash_screen.xml +11 -6
  8. package/android/src/main/res/drawable/compat_splash_screen_oneui_4.xml +13 -6
  9. package/android/src/main/res/values/colors.xml +1 -3
  10. package/android/src/main/res/values/public.xml +0 -2
  11. package/android/src/main/res/values/styles.xml +13 -19
  12. package/android/src/main/res/values-night/styles.xml +1 -1
  13. package/android/src/main/res/values-v27/colors.xml +4 -0
  14. package/android/src/main/res/values-v27/styles.xml +1 -6
  15. package/android/src/main/res/values-v29/styles.xml +1 -11
  16. package/android/src/main/res/values-v30/styles.xml +1 -9
  17. package/android/src/main/res/values-v31/styles.xml +1 -1
  18. package/android/src/newarch/com/zoontek/rnbootsplash/RNBootSplashModule.kt +2 -2
  19. package/android/src/oldarch/com/zoontek/rnbootsplash/RNBootSplashModule.kt +3 -3
  20. package/app.plugin.js +1 -1
  21. package/cli.js +2 -11
  22. package/dist/commonjs/addon/index.js +49 -49
  23. package/dist/commonjs/addon/index.js.map +1 -1
  24. package/dist/commonjs/generate.js +797 -553
  25. package/dist/commonjs/generate.js.map +1 -1
  26. package/dist/commonjs/index.js +1 -1
  27. package/dist/commonjs/index.js.map +1 -1
  28. package/dist/commonjs/specs/NativeRNBootSplash.js.map +1 -0
  29. package/dist/commonjs/{NativeRNBootSplash.web.js → specs/NativeRNBootSplash.web.js} +1 -1
  30. package/dist/commonjs/specs/NativeRNBootSplash.web.js.map +1 -0
  31. package/dist/module/addon/index.js +49 -49
  32. package/dist/module/addon/index.js.map +1 -1
  33. package/dist/module/generate.js +798 -551
  34. package/dist/module/generate.js.map +1 -1
  35. package/dist/module/index.js +1 -1
  36. package/dist/module/index.js.map +1 -1
  37. package/dist/module/specs/NativeRNBootSplash.js.map +1 -0
  38. package/dist/module/{NativeRNBootSplash.web.js → specs/NativeRNBootSplash.web.js} +1 -1
  39. package/dist/module/specs/NativeRNBootSplash.web.js.map +1 -0
  40. package/dist/typescript/addon/index.d.ts +22 -2
  41. package/dist/typescript/addon/index.d.ts.map +1 -1
  42. package/dist/typescript/generate.d.ts +54 -35
  43. package/dist/typescript/generate.d.ts.map +1 -1
  44. package/dist/typescript/index.d.ts +2 -2
  45. package/dist/typescript/index.d.ts.map +1 -1
  46. package/dist/typescript/{NativeRNBootSplash.d.ts → specs/NativeRNBootSplash.d.ts} +1 -1
  47. package/dist/typescript/specs/NativeRNBootSplash.d.ts.map +1 -0
  48. package/dist/typescript/{NativeRNBootSplash.web.d.ts → specs/NativeRNBootSplash.web.d.ts} +1 -1
  49. package/dist/typescript/specs/NativeRNBootSplash.web.d.ts.map +1 -0
  50. package/ios/RNBootSplash.mm +4 -10
  51. package/package.json +25 -30
  52. package/src/generate.ts +1045 -805
  53. package/src/index.ts +7 -7
  54. package/src/{NativeRNBootSplash.ts → specs/NativeRNBootSplash.ts} +1 -1
  55. package/src/{NativeRNBootSplash.web.ts → specs/NativeRNBootSplash.web.ts} +1 -1
  56. package/android/src/main/AndroidManifestNew.xml +0 -3
  57. package/android/src/main/res/drawable-v23/compat_splash_screen.xml +0 -19
  58. package/android/src/main/res/drawable-v23/compat_splash_screen_oneui_4.xml +0 -21
  59. package/android/src/main/res/values-v23/styles.xml +0 -13
  60. package/dist/commonjs/NativeRNBootSplash.js.map +0 -1
  61. package/dist/commonjs/NativeRNBootSplash.web.js.map +0 -1
  62. package/dist/commonjs/expo.js +0 -348
  63. package/dist/commonjs/expo.js.map +0 -1
  64. package/dist/module/NativeRNBootSplash.js.map +0 -1
  65. package/dist/module/NativeRNBootSplash.web.js.map +0 -1
  66. package/dist/module/expo.js +0 -341
  67. package/dist/module/expo.js.map +0 -1
  68. package/dist/typescript/NativeRNBootSplash.d.ts.map +0 -1
  69. package/dist/typescript/NativeRNBootSplash.web.d.ts.map +0 -1
  70. package/dist/typescript/expo.d.ts +0 -11
  71. package/dist/typescript/expo.d.ts.map +0 -1
  72. package/react-native.config.js +0 -101
  73. package/src/expo.ts +0 -439
  74. /package/dist/commonjs/{NativeRNBootSplash.js → specs/NativeRNBootSplash.js} +0 -0
  75. /package/dist/module/{NativeRNBootSplash.js → specs/NativeRNBootSplash.js} +0 -0
package/src/generate.ts CHANGED
@@ -1,40 +1,37 @@
1
1
  import * as Expo from "@expo/config-plugins";
2
- import plist from "@expo/plist";
2
+ import { assignColorValue } from "@expo/config-plugins/build/android/Colors";
3
+ import { addImports } from "@expo/config-plugins/build/android/codeMod";
4
+ import { mergeContents } from "@expo/config-plugins/build/utils/generateCode";
5
+ import ExpoPlist from "@expo/plist";
3
6
  import { projectConfig as getAndroidProjectConfig } from "@react-native-community/cli-config-android";
4
7
  import { getProjectConfig as getAppleProjectConfig } from "@react-native-community/cli-config-apple";
5
8
  import { findProjectRoot } from "@react-native-community/cli-tools";
6
9
  import childProcess from "child_process";
7
10
  import crypto from "crypto";
8
- import detectIndent from "detect-indent";
11
+ import detectIndent, { type Indent } from "detect-indent";
9
12
  import fs from "fs-extra";
10
13
  import { HTMLElement, parse as parseHtml } from "node-html-parser";
11
14
  import path from "path";
12
15
  import pc from "picocolors";
13
- import { Options as PrettierOptions } from "prettier";
16
+ import type { Options as PrettierOptions } from "prettier";
14
17
  import * as htmlPlugin from "prettier/plugins/html";
15
18
  import * as cssPlugin from "prettier/plugins/postcss";
16
19
  import * as prettier from "prettier/standalone";
17
20
  import semver from "semver";
18
- import sharp, { Sharp } from "sharp";
21
+ import sharp, { type Sharp } from "sharp";
19
22
  import { dedent } from "ts-dedent";
20
23
  import util from "util";
21
- import formatXml, { XMLFormatterOptions } from "xml-formatter";
22
- import { Manifest } from ".";
24
+ import formatXml, { type XMLFormatterOptions } from "xml-formatter";
25
+ import type { Manifest } from ".";
23
26
 
24
- const workingPath = process.env.INIT_CWD ?? process.env.PWD ?? process.cwd();
25
- const projectRoot = findProjectRoot(workingPath);
27
+ const PACKAGE_NAME = "react-native-bootsplash";
26
28
 
27
- const getIOSProjectConfig = getAppleProjectConfig({ platformName: "ios" });
28
- const ios = getIOSProjectConfig(projectRoot, {});
29
- const android = getAndroidProjectConfig(projectRoot);
29
+ let isExpo = false;
30
30
 
31
- type PackageJson = {
32
- version?: string;
33
- dependencies?: Record<string, string>;
34
- };
31
+ const workingPath = process.env.INIT_CWD ?? process.env.PWD ?? process.cwd();
32
+ const projectRoot = findProjectRoot(workingPath);
35
33
 
36
- type ProjectType = "detect" | "bare" | "expo";
37
- type Platforms = ("android" | "ios" | "web")[];
34
+ export type Platforms = ("android" | "ios" | "web")[];
38
35
 
39
36
  export type RGBColor = {
40
37
  R: string;
@@ -54,22 +51,30 @@ const exec = (cmd: string) =>
54
51
 
55
52
  export const log = {
56
53
  error: (text: string) => {
57
- console.log(pc.red(`❌ ${text}`));
54
+ console.log(
55
+ pc.red(isExpo ? `❌ [${PACKAGE_NAME}] ${text}` : `❌ ${text}`),
56
+ );
58
57
  },
59
58
  title: (emoji: string, text: string) => {
60
- console.log(`\n${emoji} ${pc.underline(pc.bold(text))}`);
59
+ if (!isExpo) {
60
+ console.log(`\n${emoji} ${pc.underline(pc.bold(text))}`);
61
+ }
61
62
  },
62
63
  warn: (text: string) => {
63
- console.log(pc.yellow(`⚠️ ${text}`));
64
- },
65
- write: (filePath: string, dimensions?: { width: number; height: number }) => {
66
64
  console.log(
67
- ` ${path.relative(workingPath, filePath)}` +
68
- (dimensions != null
69
- ? ` (${dimensions.width}x${dimensions.height})`
70
- : ""),
65
+ pc.yellow(isExpo ? `⚠️ [${PACKAGE_NAME}] ${text}` : `⚠️ ${text}`),
71
66
  );
72
67
  },
68
+ write: (filePath: string, dimensions?: { width: number; height: number }) => {
69
+ if (!isExpo) {
70
+ console.log(
71
+ ` ${path.relative(workingPath, filePath)}` +
72
+ (dimensions != null
73
+ ? ` (${dimensions.width}x${dimensions.height})`
74
+ : ""),
75
+ );
76
+ }
77
+ },
73
78
  };
74
79
 
75
80
  const parseColor = (value: string): Color => {
@@ -94,21 +99,12 @@ const parseColor = (value: string): Color => {
94
99
  return { hex: hex.toLowerCase(), rgb };
95
100
  };
96
101
 
97
- const getStoryboard = ({
98
- logoHeight,
99
- logoWidth,
100
- background: { R, G, B },
101
- fileNameSuffix,
102
- }: {
103
- logoHeight: number;
104
- logoWidth: number;
105
- background: Color["rgb"];
106
- fileNameSuffix: string;
107
- }) => {
102
+ const getStoryboard = (props: Props) => {
103
+ const { background, logo, fileNameSuffix } = props;
104
+
105
+ const { R, G, B } = background.rgb;
108
106
  const frameWidth = 375;
109
107
  const frameHeight = 667;
110
- const logoX = (frameWidth - logoWidth) / 2;
111
- const logoY = (frameHeight - logoHeight) / 2;
112
108
 
113
109
  return dedent`
114
110
  <?xml version="1.0" encoding="UTF-8"?>
@@ -131,7 +127,7 @@ const getStoryboard = ({
131
127
  <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
132
128
  <subviews>
133
129
  <imageView autoresizesSubviews="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="BootSplashLogo-${fileNameSuffix}" translatesAutoresizingMaskIntoConstraints="NO" id="3lX-Ut-9ad">
134
- <rect key="frame" x="${logoX}" y="${logoY}" width="${logoWidth}" height="${logoHeight}"/>
130
+ <rect key="frame" x="${(frameWidth - logo.width) / 2}" y="${(frameHeight - logo.height) / 2}" width="${logo.width}" height="${logo.height}"/>
135
131
  <accessibility key="accessibilityConfiguration">
136
132
  <accessibilityTraits key="traits" image="YES" notEnabled="YES"/>
137
133
  </accessibility>
@@ -151,7 +147,7 @@ const getStoryboard = ({
151
147
  </scene>
152
148
  </scenes>
153
149
  <resources>
154
- <image name="BootSplashLogo-${fileNameSuffix}" width="${logoWidth}" height="${logoHeight}"/>
150
+ <image name="BootSplashLogo-${fileNameSuffix}" width="${logo.width}" height="${logo.height}"/>
155
151
  <namedColor name="BootSplashBackground-${fileNameSuffix}">
156
152
  <color red="${R}" green="${G}" blue="${B}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
157
153
  </namedColor>
@@ -171,18 +167,6 @@ export const hfs = {
171
167
  rm: (path: string) => fs.rmSync(path, { force: true, recursive: true }),
172
168
  text: (path: string) => fs.readFileSync(path, "utf-8"),
173
169
 
174
- copy: (src: string, dest: string) => {
175
- if (hfs.isDir(src) || !hfs.exists(dest)) {
176
- return fs.copySync(src, dest, { overwrite: true });
177
- }
178
-
179
- const srcBuffer = fs.readFileSync(src);
180
- const destBuffer = fs.readFileSync(dest);
181
-
182
- if (!srcBuffer.equals(destBuffer)) {
183
- return fs.copySync(src, dest, { overwrite: true });
184
- }
185
- },
186
170
  ensureDir: (dir: string) => {
187
171
  fs.mkdirSync(dir, { recursive: true });
188
172
  },
@@ -192,65 +176,12 @@ export const hfs = {
192
176
  },
193
177
  };
194
178
 
195
- // Adapted from https://github.com/square/find-yarn-workspace-root
196
- const findUp = <T>(from: string, matcher: (dir: string) => T | undefined) => {
197
- let previous: string | undefined;
198
- let current = path.normalize(from);
199
-
200
- do {
201
- const found = matcher(current);
202
-
203
- if (typeof found !== "undefined") {
204
- return found;
205
- }
206
-
207
- previous = current;
208
- current = path.dirname(current);
209
- } while (current !== previous);
210
- };
211
-
212
- export const getExpoConfig = (from: string): { isExpo: boolean } => {
213
- const hasDependency =
214
- findUp(from, (dir) => {
215
- const pkgPath = path.resolve(dir, "package.json");
216
-
217
- if (fs.existsSync(pkgPath)) {
218
- try {
219
- const pkg = hfs.json(pkgPath) as PackageJson;
220
- return pkg.dependencies?.expo != null;
221
- } catch {} // eslint-disable-line no-empty
222
- }
223
- }) ?? false;
224
-
225
- if (!hasDependency) {
226
- return { isExpo: false };
227
- }
228
-
229
- const version = findUp(from, (dir) => {
230
- const pkgPath = path.resolve(dir, "node_modules", "expo", "package.json");
231
-
232
- if (fs.existsSync(pkgPath)) {
233
- try {
234
- const pkg = hfs.json(pkgPath) as PackageJson;
235
- return pkg.version;
236
- } catch {} // eslint-disable-line no-empty
237
- }
238
- });
239
-
240
- if (version == null || semver.lt(version, "51.0.20")) {
241
- log.error("Requires Expo 51.0.20 (or higher)");
242
- process.exit(1);
243
- }
244
-
245
- return { isExpo: true };
246
- };
247
-
248
179
  export const writeJson = (filePath: string, content: object) => {
249
180
  hfs.write(filePath, JSON.stringify(content, null, 2));
250
181
  log.write(filePath);
251
182
  };
252
183
 
253
- type FormatOptions = { indent?: detectIndent.Indent } & (
184
+ type FormatOptions = { indent?: Indent } & (
254
185
  | {
255
186
  formatter: "prettier";
256
187
  selfClosingTags?: boolean;
@@ -321,148 +252,16 @@ export const writeXmlLike = async (
321
252
  }
322
253
  };
323
254
 
324
- export const cleanIOSAssets = (dir: string) => {
325
- hfs
326
- .readDir(dir)
327
- .filter((file) => file === "Colors.xcassets" || file === "Images.xcassets")
328
- .map((file) => path.join(dir, file))
329
- .flatMap((dir) =>
330
- hfs
331
- .readDir(dir)
332
- .filter((file) => file.startsWith("BootSplash"))
333
- .map((file) => path.join(dir, file)),
334
- )
335
- .forEach((file) => {
336
- hfs.rm(file);
337
- });
338
- };
339
-
340
- const getImageBase64 = async (
341
- image: Sharp | undefined,
342
- width: number,
343
- ): Promise<string> => {
344
- if (image == null) {
345
- return "";
346
- }
347
-
348
- const buffer = await image
349
- .clone()
350
- .resize(width)
351
- .png({ quality: 100 })
352
- .toBuffer();
353
-
354
- return buffer.toString("base64");
355
- };
356
-
357
- const getFileNameSuffix = async ({
358
- background,
359
- brand,
360
- brandWidth,
361
- darkBackground,
362
- darkBrand,
363
- darkLogo,
364
- logo,
365
- logoWidth,
366
- }: {
367
- background: Color;
368
- brand: Sharp | undefined;
369
- brandWidth: number;
370
- darkBackground: Color | undefined;
371
- darkBrand: Sharp | undefined;
372
- darkLogo: Sharp | undefined;
373
- logo: Sharp;
374
- logoWidth: number;
375
- }) => {
376
- const [logoHash, darkLogoHash, brandHash, darkBrandHash] = await Promise.all([
377
- getImageBase64(logo, logoWidth),
378
- getImageBase64(darkLogo, logoWidth),
379
- getImageBase64(brand, brandWidth),
380
- getImageBase64(darkBrand, brandWidth),
381
- ]);
382
-
383
- const record: Record<string, string> = {
384
- background: background.hex,
385
- darkBackground: darkBackground?.hex ?? "",
386
- logo: logoHash,
387
- darkLogo: darkLogoHash,
388
- brand: brandHash,
389
- darkBrand: darkBrandHash,
390
- };
391
-
392
- const stableKey = Object.keys(record)
393
- .sort()
394
- .map((key) => record[key])
395
- .join();
396
-
397
- return crypto
398
- .createHash("shake256", { outputLength: 3 })
399
- .update(stableKey)
400
- .digest("hex")
401
- .toLowerCase();
402
- };
403
-
404
- const ensureSupportedFormat = async (
405
- name: string,
406
- image: Sharp | undefined,
407
- ) => {
408
- if (image == null) {
409
- return;
410
- }
411
-
412
- const { format } = await image.metadata();
413
-
414
- if (format !== "png" && format !== "svg") {
415
- log.error(`${name} image file format (${format}) is not supported`);
416
- process.exit(1);
417
- }
418
- };
419
-
420
255
  const getAndroidOutputPath = ({
421
- assetsOutputPath,
422
- brandHeight,
423
- brandWidth,
424
256
  flavor,
425
- isExpo,
426
- logoHeight,
427
- logoWidth,
428
257
  platforms,
429
258
  }: {
430
- assetsOutputPath: string;
431
- brandHeight: number;
432
- brandWidth: number;
433
259
  flavor: string;
434
- isExpo: boolean;
435
- logoHeight: number;
436
- logoWidth: number;
437
260
  platforms: Platforms;
438
261
  }) => {
439
- if (!platforms.includes("android")) {
440
- return;
441
- }
442
-
443
- const withSizeChecks = (assetsOutputPath: string) => {
444
- if (logoWidth > 192 || logoHeight > 192) {
445
- return log.warn(
446
- "Logo size exceeding 192x192dp will be cropped by Android. Skipping Android assets generation…",
447
- );
448
- }
449
- if (brandWidth > 200 || brandHeight > 80) {
450
- return log.warn(
451
- "Brand size exceeding 200x80dp will be cropped by Android. Skipping Android assets generation…",
452
- );
453
- }
454
-
455
- if (logoWidth > 134 || logoHeight > 134) {
456
- log.warn("Logo size exceeds 134x134dp. It might be cropped by Android.");
457
- }
458
-
459
- return assetsOutputPath;
460
- };
262
+ const android = getAndroidProjectConfig(projectRoot);
461
263
 
462
- if (isExpo) {
463
- return withSizeChecks(path.resolve(assetsOutputPath, "android"));
464
- }
465
- if (android == null) {
264
+ if (!platforms.includes("android") || android == null) {
466
265
  return;
467
266
  }
468
267
 
@@ -483,25 +282,13 @@ const getAndroidOutputPath = ({
483
282
  );
484
283
  }
485
284
 
486
- return withSizeChecks(androidOutputPath);
285
+ return androidOutputPath;
487
286
  };
488
287
 
489
- const getIOSOutputPath = ({
490
- assetsOutputPath,
491
- isExpo,
492
- platforms,
493
- }: {
494
- assetsOutputPath: string;
495
- isExpo: boolean;
496
- platforms: Platforms;
497
- }) => {
498
- if (!platforms.includes("ios")) {
499
- return;
500
- }
501
- if (isExpo) {
502
- return path.resolve(assetsOutputPath, "ios");
503
- }
504
- if (ios == null) {
288
+ const getIOSOutputPath = ({ platforms }: { platforms: Platforms }) => {
289
+ const ios = getAppleProjectConfig({ platformName: "ios" })(projectRoot, {});
290
+
291
+ if (!platforms.includes("ios") || ios == null) {
505
292
  return;
506
293
  }
507
294
  if (ios.xcodeProject == null) {
@@ -524,12 +311,10 @@ const getIOSOutputPath = ({
524
311
  return iosOutputPath;
525
312
  };
526
313
 
527
- const getHtmlTemplatePath = async ({
528
- isExpo,
314
+ const getHtmlTemplatePath = ({
529
315
  html,
530
316
  platforms,
531
317
  }: {
532
- isExpo: boolean;
533
318
  html: string;
534
319
  platforms: Platforms;
535
320
  }) => {
@@ -537,24 +322,6 @@ const getHtmlTemplatePath = async ({
537
322
  return;
538
323
  }
539
324
 
540
- if (isExpo) {
541
- const htmlTemplatePath = path.resolve(workingPath, html);
542
-
543
- const htmlTemplateRelativePath = path.relative(
544
- workingPath,
545
- htmlTemplatePath,
546
- );
547
-
548
- if (
549
- htmlTemplateRelativePath === "public/index.html" &&
550
- !hfs.exists(htmlTemplatePath)
551
- ) {
552
- const cmd = `npx expo customize ${htmlTemplateRelativePath}`;
553
- console.log(pc.dim(`Running ${cmd}`));
554
- await exec(cmd);
555
- }
556
- }
557
-
558
325
  const htmlTemplatePath = path.resolve(workingPath, html);
559
326
 
560
327
  if (!hfs.exists(htmlTemplatePath)) {
@@ -562,84 +329,38 @@ const getHtmlTemplatePath = async ({
562
329
  `No ${path.relative(
563
330
  workingPath,
564
331
  htmlTemplatePath,
565
- )} file found. Skipping HTML + CSS generation…`,
332
+ )} found. Skipping HTML + CSS generation…`,
566
333
  );
567
334
  }
568
335
 
569
336
  return htmlTemplatePath;
570
337
  };
571
338
 
572
- const getImageHeight = (
573
- image: Sharp | undefined,
574
- width: number,
575
- ): Promise<number> => {
576
- if (image == null) {
577
- return Promise.resolve(0);
578
- }
579
-
580
- return image
581
- .clone()
582
- .resize(width)
583
- .toBuffer()
584
- .then((buffer) => sharp(buffer).metadata())
585
- .then(({ height = 0 }) => Math.round(height));
586
- };
587
-
588
- export type AddonConfig = {
589
- licenseKey: string;
590
- isExpo: boolean;
591
- fileNameSuffix: string;
592
-
593
- androidOutputPath: string | void;
594
- iosOutputPath: string | void;
595
- htmlTemplatePath: string | void;
596
- assetsOutputPath: string;
597
-
598
- logoPath: string;
599
- darkLogoPath: string | undefined;
600
- brandPath: string | undefined;
601
- darkBrandPath: string | undefined;
602
-
603
- logoHeight: number;
604
- logoWidth: number;
605
- brandHeight: number;
606
- brandWidth: number;
607
-
608
- background: Color;
609
- logo: Sharp;
610
- brand: Sharp | undefined;
339
+ const getInfoPlistPath = ({
340
+ iosOutputPath,
341
+ plist,
342
+ }: {
343
+ iosOutputPath: string;
344
+ plist: string | undefined;
345
+ }) => {
346
+ if (plist != null) {
347
+ const infoPlistPath = path.resolve(workingPath, plist);
611
348
 
612
- darkBackground: Color | undefined;
613
- darkLogo: Sharp | undefined;
614
- darkBrand: Sharp | undefined;
615
- };
349
+ if (!hfs.exists(infoPlistPath)) {
350
+ return log.warn(`No ${path.relative(workingPath, infoPlistPath)} found`);
351
+ }
616
352
 
617
- const requireAddon = ():
618
- | { execute: (config: AddonConfig) => Promise<void> }
619
- | undefined => {
620
- try {
621
- return require("./addon"); // eslint-disable-line
622
- } catch {
623
- return;
353
+ return infoPlistPath;
624
354
  }
355
+
356
+ return path.resolve(iosOutputPath, "Info.plist");
625
357
  };
626
358
 
627
- export const generate = async ({
628
- projectType,
629
- platforms,
630
- html,
631
- flavor,
632
- licenseKey,
633
- ...args
634
- }: {
359
+ type RawProps = {
635
360
  logo: string;
636
- projectType: ProjectType;
637
- platforms: Platforms;
638
361
  background: string;
639
362
  logoWidth: number;
640
363
  assetsOutput: string;
641
- html: string;
642
- flavor: string;
643
364
 
644
365
  licenseKey?: string;
645
366
  brand?: string;
@@ -647,44 +368,97 @@ export const generate = async ({
647
368
  darkBackground?: string;
648
369
  darkLogo?: string;
649
370
  darkBrand?: string;
650
- }) => {
651
- const isExpo =
652
- projectType === "expo" ||
653
- (projectType === "detect" && getExpoConfig(workingPath).isExpo);
654
371
 
655
- if (semver.lt(process.versions.node, "18.0.0")) {
656
- log.error("Requires Node 18 (or higher)");
372
+ android?: {
373
+ darkContentBarsStyle?: boolean;
374
+ };
375
+ };
376
+
377
+ export type Asset = {
378
+ path: string;
379
+ image: Sharp;
380
+ hash: string;
381
+ height: number;
382
+ width: number;
383
+ };
384
+
385
+ const toAsset = async (filePath: string, width: number): Promise<Asset> => {
386
+ const image = sharp(filePath);
387
+ const { format } = await image.metadata();
388
+
389
+ if (format !== "png" && format !== "svg") {
390
+ log.error(
391
+ `${path.basename(filePath)} image file format (${format}) is not supported`,
392
+ );
393
+ process.exit(1);
394
+ }
395
+
396
+ const [height, hash] = await Promise.all([
397
+ image
398
+ .clone()
399
+ .resize(width)
400
+ .toBuffer()
401
+ .then((buffer) => sharp(buffer).metadata())
402
+ .then(({ height = 0 }) => Math.round(height)),
403
+
404
+ image
405
+ .clone()
406
+ .resize(width)
407
+ .png({ quality: 100 })
408
+ .toBuffer()
409
+ .then((buffer) => buffer.toString("base64")),
410
+ ]);
411
+
412
+ return {
413
+ path: filePath,
414
+ image,
415
+ hash,
416
+ height,
417
+ width,
418
+ };
419
+ };
420
+
421
+ const transformProps = async (
422
+ rootPath: string,
423
+ { android = {}, licenseKey, ...rawProps }: RawProps,
424
+ ) => {
425
+ if (semver.lt(process.versions.node, "20.0.0")) {
426
+ log.error("Requires Node 20 (or higher)");
657
427
  process.exit(1);
658
428
  }
659
429
 
660
- const logoPath = path.resolve(workingPath, args.logo);
430
+ const assetsOutputPath = path.resolve(rootPath, rawProps.assetsOutput);
431
+ const logoPath = path.resolve(rootPath, rawProps.logo);
661
432
 
662
433
  const darkLogoPath =
663
- args.darkLogo != null
664
- ? path.resolve(workingPath, args.darkLogo)
434
+ rawProps.darkLogo != null
435
+ ? path.resolve(rootPath, rawProps.darkLogo)
665
436
  : undefined;
666
437
 
667
438
  const brandPath =
668
- args.brand != null ? path.resolve(workingPath, args.brand) : undefined;
439
+ rawProps.brand != null ? path.resolve(rootPath, rawProps.brand) : undefined;
669
440
 
670
441
  const darkBrandPath =
671
- args.darkBrand != null
672
- ? path.resolve(workingPath, args.darkBrand)
442
+ rawProps.darkBrand != null
443
+ ? path.resolve(rootPath, rawProps.darkBrand)
673
444
  : undefined;
674
445
 
675
- const assetsOutputPath = path.resolve(workingPath, args.assetsOutput);
446
+ const logoWidth = rawProps.logoWidth - (rawProps.logoWidth % 2);
447
+ const brandWidth = rawProps.brandWidth - (rawProps.brandWidth % 2);
676
448
 
677
- const logo = sharp(logoPath);
678
- const darkLogo = darkLogoPath != null ? sharp(darkLogoPath) : undefined;
679
- const brand = brandPath != null ? sharp(brandPath) : undefined;
680
- const darkBrand = darkBrandPath != null ? sharp(darkBrandPath) : undefined;
449
+ const [logo, darkLogo, brand, darkBrand] = await Promise.all([
450
+ toAsset(logoPath, logoWidth),
451
+ darkLogoPath != null ? toAsset(darkLogoPath, logoWidth) : undefined,
452
+ brandPath != null ? toAsset(brandPath, brandWidth) : undefined,
453
+ darkBrandPath != null ? toAsset(darkBrandPath, brandWidth) : undefined,
454
+ ]);
681
455
 
682
- const background = parseColor(args.background);
683
- const logoWidth = args.logoWidth - (args.logoWidth % 2);
684
- const brandWidth = args.brandWidth - (args.brandWidth % 2);
456
+ const background = parseColor(rawProps.background);
685
457
 
686
458
  const darkBackground =
687
- args.darkBackground != null ? parseColor(args.darkBackground) : undefined;
459
+ rawProps.darkBackground != null
460
+ ? parseColor(rawProps.darkBackground)
461
+ : undefined;
688
462
 
689
463
  const executeAddon =
690
464
  brand != null ||
@@ -698,15 +472,21 @@ export const generate = async ({
698
472
  );
699
473
  }
700
474
 
475
+ const optionNames = {
476
+ brand: isExpo ? "brand" : "--brand",
477
+ darkBackground: isExpo ? "darkBackground" : "--dark-background",
478
+ darkLogo: isExpo ? "darkLogo" : "--dark-logo",
479
+ darkBrand: isExpo ? "darkBrand" : "--dark-brand",
480
+ };
481
+
701
482
  if (licenseKey == null && executeAddon) {
702
483
  const options = [
703
- brand != null ? "brand" : "",
704
- darkBackground != null ? "dark-background" : "",
705
- darkLogo != null ? "dark-logo" : "",
706
- darkBrand != null ? "dark-brand" : "",
484
+ brand != null ? optionNames.brand : "",
485
+ darkBackground != null ? optionNames.darkBackground : "",
486
+ darkLogo != null ? optionNames.darkLogo : "",
487
+ darkBrand != null ? optionNames.darkBrand : "",
707
488
  ]
708
489
  .filter((option) => option !== "")
709
- .map((option) => `--${option}`)
710
490
  .join(", ");
711
491
 
712
492
  log.error(`You need to specify a license key in order to use ${options}.`);
@@ -714,483 +494,348 @@ export const generate = async ({
714
494
  }
715
495
 
716
496
  if (brand == null && darkBrand != null) {
717
- log.error("--dark-brand option couldn't be used without --brand.");
497
+ log.error(
498
+ `${optionNames.darkBrand} option couldn't be used without ${optionNames.brand}.`,
499
+ );
500
+
718
501
  process.exit(1);
719
502
  }
720
503
 
721
- await ensureSupportedFormat("Logo", logo);
722
- await ensureSupportedFormat("Dark logo", darkLogo);
723
- await ensureSupportedFormat("Brand", brand);
724
- await ensureSupportedFormat("Dark brand", darkBrand);
725
-
726
- const logoHeight = await getImageHeight(logo, logoWidth);
727
- const brandHeight = await getImageHeight(brand, brandWidth);
728
-
729
- if (logoWidth < args.logoWidth) {
504
+ if (logoWidth < rawProps.logoWidth) {
730
505
  log.warn(
731
506
  `Logo width must be a multiple of 2. It has been rounded to ${logoWidth}dp.`,
732
507
  );
733
508
  }
734
- if (brandWidth < args.brandWidth) {
509
+ if (brandWidth < rawProps.brandWidth) {
735
510
  log.warn(
736
511
  `Brand width must be a multiple of 2. It has been rounded to ${brandWidth}dp.`,
737
512
  );
738
513
  }
739
514
 
740
- const fileNameSuffix = await getFileNameSuffix({
515
+ const record: Record<string, string> = {
516
+ background: background.hex,
517
+ darkBackground: darkBackground?.hex ?? "",
518
+ logo: logo.hash,
519
+ darkLogo: darkLogo?.hash ?? "",
520
+ brand: brand?.hash ?? "",
521
+ darkBrand: darkBrand?.hash ?? "",
522
+ };
523
+
524
+ const stableKey = Object.keys(record)
525
+ .sort()
526
+ .map((key) => record[key])
527
+ .join();
528
+
529
+ const fileNameSuffix = crypto
530
+ .createHash("shake256", { outputLength: 3 })
531
+ .update(stableKey)
532
+ .digest("hex")
533
+ .toLowerCase();
534
+
535
+ return {
536
+ android,
537
+ assetsOutputPath,
538
+ licenseKey,
539
+ executeAddon,
741
540
  background,
742
- brand,
743
- brandWidth,
744
541
  darkBackground,
745
- darkBrand,
746
- darkLogo,
747
542
  logo,
748
- logoWidth,
749
- });
543
+ darkLogo,
544
+ brand,
545
+ darkBrand,
546
+ fileNameSuffix,
547
+ };
548
+ };
750
549
 
751
- const androidOutputPath = getAndroidOutputPath({
752
- assetsOutputPath,
753
- brandHeight,
754
- brandWidth,
755
- flavor,
756
- isExpo,
757
- logoHeight,
758
- logoWidth,
759
- platforms,
760
- });
550
+ export type Props = Awaited<ReturnType<typeof transformProps>>;
761
551
 
762
- const iosOutputPath = getIOSOutputPath({
763
- assetsOutputPath,
764
- isExpo,
765
- platforms,
766
- });
552
+ const writeAndroidAssets = async ({
553
+ androidOutputPath,
554
+ props,
555
+ }: {
556
+ androidOutputPath: string;
557
+ props: Props;
558
+ }) => {
559
+ const { logo, brand } = props;
767
560
 
768
- const htmlTemplatePath = await getHtmlTemplatePath({
769
- isExpo,
770
- html,
771
- platforms,
772
- });
561
+ if (logo.width > 192 || logo.height > 192) {
562
+ return log.warn(
563
+ "Logo size exceeding 192x192dp will be cropped by Android. Skipping Android assets generation…",
564
+ );
565
+ }
773
566
 
774
- if (androidOutputPath != null) {
775
- log.title("🤖", "Android");
776
-
777
- hfs.ensureDir(androidOutputPath);
778
-
779
- await Promise.all(
780
- [
781
- { ratio: 1, suffix: "mdpi" },
782
- { ratio: 1.5, suffix: "hdpi" },
783
- { ratio: 2, suffix: "xhdpi" },
784
- { ratio: 3, suffix: "xxhdpi" },
785
- { ratio: 4, suffix: "xxxhdpi" },
786
- ].map(({ ratio, suffix }) => {
787
- const drawableDirPath = path.resolve(
788
- androidOutputPath,
789
- `drawable-${suffix}`,
790
- );
791
-
792
- hfs.ensureDir(drawableDirPath);
793
-
794
- // https://developer.android.com/develop/ui/views/launch/splash-screen#dimensions
795
- const canvasSize = 288 * ratio;
796
-
797
- // https://sharp.pixelplumbing.com/api-constructor
798
- const canvas = sharp({
799
- create: {
800
- width: canvasSize,
801
- height: canvasSize,
802
- channels: 4,
803
- background: {
804
- r: 255,
805
- g: 255,
806
- b: 255,
807
- alpha: 0,
808
- },
809
- },
810
- });
567
+ if (brand != null && (brand.width > 200 || brand.height > 80)) {
568
+ return log.warn(
569
+ "Brand size exceeding 200x80dp will be cropped by Android. Skipping Android assets generation…",
570
+ );
571
+ }
811
572
 
812
- const filePath = path.resolve(drawableDirPath, "bootsplash_logo.png");
573
+ if (logo.width > 134 || logo.height > 134) {
574
+ log.warn("Logo size exceeds 134x134dp. It might be cropped by Android.");
575
+ }
813
576
 
814
- return logo
815
- .clone()
816
- .resize(logoWidth * ratio)
817
- .toBuffer()
818
- .then((input) =>
819
- canvas
820
- .composite([{ input }])
821
- .png({ quality: 100 })
822
- .toFile(filePath),
823
- )
824
- .then(() => {
825
- log.write(filePath, {
826
- width: canvasSize,
827
- height: canvasSize,
828
- });
829
- });
830
- }),
831
- );
577
+ log.title("🤖", "Android");
578
+ hfs.ensureDir(androidOutputPath);
832
579
 
833
- if (!isExpo) {
834
- const manifestXmlPath = path.resolve(
580
+ await Promise.all(
581
+ [
582
+ { ratio: 1, suffix: "mdpi" },
583
+ { ratio: 1.5, suffix: "hdpi" },
584
+ { ratio: 2, suffix: "xhdpi" },
585
+ { ratio: 3, suffix: "xxhdpi" },
586
+ { ratio: 4, suffix: "xxxhdpi" },
587
+ ].map(({ ratio, suffix }) => {
588
+ const drawableDirPath = path.resolve(
835
589
  androidOutputPath,
836
- "..",
837
- "AndroidManifest.xml",
590
+ `drawable-${suffix}`,
838
591
  );
839
592
 
840
- if (hfs.exists(manifestXmlPath)) {
841
- const manifestXml = readXmlLike(manifestXmlPath);
842
- const activities = manifestXml.root.querySelectorAll("activity");
593
+ hfs.ensureDir(drawableDirPath);
594
+
595
+ // https://developer.android.com/develop/ui/views/launch/splash-screen#dimensions
596
+ const canvasSize = 288 * ratio;
597
+
598
+ // https://sharp.pixelplumbing.com/api-constructor
599
+ const canvas = sharp({
600
+ create: {
601
+ width: canvasSize,
602
+ height: canvasSize,
603
+ channels: 4,
604
+ background: {
605
+ r: 255,
606
+ g: 255,
607
+ b: 255,
608
+ alpha: 0,
609
+ },
610
+ },
611
+ });
843
612
 
844
- for (const activity of activities) {
845
- if (activity.getAttribute("android:name") === ".MainActivity") {
846
- activity.setAttribute("android:theme", "@style/BootTheme");
847
- }
848
- }
613
+ const filePath = path.resolve(drawableDirPath, "bootsplash_logo.png");
849
614
 
850
- await writeXmlLike(manifestXmlPath, manifestXml.root.toString(), {
851
- ...manifestXml.formatOptions,
852
- formatter: "prettier",
853
- htmlWhitespaceSensitivity: "ignore",
854
- selfClosingTags: true,
855
- singleAttributePerLine: true,
615
+ return logo.image
616
+ .clone()
617
+ .resize(logo.width * ratio)
618
+ .toBuffer()
619
+ .then((input) =>
620
+ canvas.composite([{ input }]).png({ quality: 100 }).toFile(filePath),
621
+ )
622
+ .then(() => {
623
+ log.write(filePath, {
624
+ width: canvasSize,
625
+ height: canvasSize,
626
+ });
856
627
  });
857
- } else {
858
- log.warn("No AndroidManifest.xml found. Skipping…");
859
- }
860
-
861
- const valuesPath = path.resolve(androidOutputPath, "values");
862
- hfs.ensureDir(valuesPath);
863
-
864
- const colorsXmlPath = path.resolve(valuesPath, "colors.xml");
865
- const colorsXmlEntry = `<color name="bootsplash_background">${background.hex}</color>`;
628
+ }),
629
+ );
630
+ };
866
631
 
867
- if (hfs.exists(colorsXmlPath)) {
868
- const colorsXml = readXmlLike(colorsXmlPath);
869
- const nextColor = parseHtml(colorsXmlEntry);
632
+ const writeIOSAssets = async ({
633
+ iosOutputPath,
634
+ props,
635
+ }: {
636
+ iosOutputPath: string;
637
+ props: Props;
638
+ }) => {
639
+ const { background, logo, fileNameSuffix } = props;
870
640
 
871
- const prevColor = colorsXml.root.querySelector(
872
- 'color[name="bootsplash_background"]',
873
- );
641
+ log.title("🍏", "iOS");
642
+ hfs.ensureDir(iosOutputPath);
874
643
 
875
- if (prevColor != null) {
876
- prevColor.replaceWith(nextColor);
877
- } else {
878
- colorsXml.root.querySelector("resources")?.appendChild(nextColor);
879
- }
880
-
881
- await writeXmlLike(colorsXmlPath, colorsXml.root.toString(), {
882
- ...colorsXml.formatOptions,
883
- formatter: "xmlFormatter",
884
- });
885
- } else {
886
- await writeXmlLike(
887
- colorsXmlPath,
888
- `<resources>${colorsXmlEntry}</resources>`,
889
- { formatter: "xmlFormatter" },
890
- );
891
- }
892
-
893
- const stylesXmlPath = path.resolve(valuesPath, "styles.xml");
894
-
895
- if (hfs.exists(stylesXmlPath)) {
896
- const stylesXml = readXmlLike(stylesXmlPath);
897
-
898
- const prevStyle = stylesXml.root.querySelector(
899
- 'style[name="BootTheme"]',
900
- );
901
-
902
- const parent = prevStyle?.getAttribute("parent") ?? "Theme.BootSplash";
903
-
904
- const extraItems = parseHtml(
905
- prevStyle?.text
906
- .split("\n")
907
- .map((line) => line.trim())
908
- .join("") ?? "",
909
- )
910
- .childNodes.filter((node) => {
911
- if (!(node instanceof HTMLElement)) {
912
- return true;
913
- }
914
-
915
- const name = node.getAttribute("name");
916
-
917
- return (
918
- name !== "bootSplashBackground" &&
919
- name !== "bootSplashLogo" &&
920
- name !== "bootSplashBrand" &&
921
- name !== "postBootSplashTheme"
922
- );
923
- })
924
- .map((node) => node.toString());
925
-
926
- const styleItems: string[] = [
927
- ...(extraItems.length > 0 ? [...extraItems, ""] : []),
928
-
929
- '<item name="bootSplashBackground">@color/bootsplash_background</item>',
930
- '<item name="bootSplashLogo">@drawable/bootsplash_logo</item>',
931
-
932
- ...(brand != null && brandPath != null
933
- ? ['<item name="bootSplashBrand">@drawable/bootsplash_brand</item>']
934
- : []),
935
-
936
- '<item name="postBootSplashTheme">@style/AppTheme</item>',
937
- ];
938
-
939
- const nextStyle = parseHtml(dedent`
940
- <style name="BootTheme" parent="${parent}">
941
- ${styleItems.join("\n")}
942
- </style>
943
- `);
944
-
945
- prevStyle?.remove(); // remove the existing style
946
- stylesXml.root.querySelector("resources")?.appendChild(nextStyle);
947
-
948
- await writeXmlLike(stylesXmlPath, stylesXml.root.toString(), {
949
- ...stylesXml.formatOptions,
950
- formatter: "prettier",
951
- htmlWhitespaceSensitivity: "ignore",
952
- });
953
- } else {
954
- log.warn("No styles.xml found. Skipping…");
955
- }
956
- }
957
- }
958
-
959
- if (iosOutputPath != null) {
960
- log.title("🍏", "iOS");
644
+ // clean existing assets
645
+ hfs
646
+ .readDir(iosOutputPath)
647
+ .filter((file) => file === "Colors.xcassets" || file === "Images.xcassets")
648
+ .map((file) => path.join(iosOutputPath, file))
649
+ .flatMap((dir) =>
650
+ hfs
651
+ .readDir(dir)
652
+ .filter((file) => file.startsWith("BootSplash"))
653
+ .map((file) => path.join(dir, file)),
654
+ )
655
+ .forEach((file) => {
656
+ hfs.rm(file);
657
+ });
961
658
 
962
- hfs.ensureDir(iosOutputPath);
963
- cleanIOSAssets(iosOutputPath);
659
+ const storyboardPath = path.resolve(iosOutputPath, "BootSplash.storyboard");
964
660
 
965
- const storyboardPath = path.resolve(iosOutputPath, "BootSplash.storyboard");
661
+ await writeXmlLike(storyboardPath, getStoryboard(props), {
662
+ formatter: "xmlFormatter",
663
+ whiteSpaceAtEndOfSelfclosingTag: false,
664
+ });
966
665
 
967
- const colorsSetPath = path.resolve(
968
- iosOutputPath,
969
- "Colors.xcassets",
970
- `BootSplashBackground-${fileNameSuffix}.colorset`,
971
- );
666
+ const colorsSetPath = path.resolve(
667
+ iosOutputPath,
668
+ "Colors.xcassets",
669
+ `BootSplashBackground-${fileNameSuffix}.colorset`,
670
+ );
972
671
 
973
- const imageSetPath = path.resolve(
974
- iosOutputPath,
975
- "Images.xcassets",
976
- `BootSplashLogo-${fileNameSuffix}.imageset`,
977
- );
672
+ hfs.ensureDir(colorsSetPath);
978
673
 
979
- hfs.ensureDir(colorsSetPath);
980
- hfs.ensureDir(imageSetPath);
981
-
982
- await writeXmlLike(
983
- storyboardPath,
984
- getStoryboard({
985
- logoHeight,
986
- logoWidth,
987
- background: background.rgb,
988
- fileNameSuffix,
989
- }),
674
+ writeJson(path.resolve(colorsSetPath, "Contents.json"), {
675
+ colors: [
990
676
  {
991
- formatter: "xmlFormatter",
992
- whiteSpaceAtEndOfSelfclosingTag: false,
993
- },
994
- );
995
-
996
- writeJson(path.resolve(colorsSetPath, "Contents.json"), {
997
- colors: [
998
- {
999
- idiom: "universal",
1000
- color: {
1001
- "color-space": "srgb",
1002
- components: {
1003
- blue: background.rgb.B,
1004
- green: background.rgb.G,
1005
- red: background.rgb.R,
1006
- alpha: "1.000",
1007
- },
677
+ idiom: "universal",
678
+ color: {
679
+ "color-space": "srgb",
680
+ components: {
681
+ blue: background.rgb.B,
682
+ green: background.rgb.G,
683
+ red: background.rgb.R,
684
+ alpha: "1.000",
1008
685
  },
1009
686
  },
1010
- ],
1011
- info: {
1012
- author: "xcode",
1013
- version: 1,
1014
- },
1015
- });
1016
-
1017
- const logoFileName = `logo-${fileNameSuffix}`;
1018
-
1019
- writeJson(path.resolve(imageSetPath, "Contents.json"), {
1020
- images: [
1021
- {
1022
- idiom: "universal",
1023
- filename: `${logoFileName}.png`,
1024
- scale: "1x",
1025
- },
1026
- {
1027
- idiom: "universal",
1028
- filename: `${logoFileName}@2x.png`,
1029
- scale: "2x",
1030
- },
1031
- {
1032
- idiom: "universal",
1033
- filename: `${logoFileName}@3x.png`,
1034
- scale: "3x",
1035
- },
1036
- ],
1037
- info: {
1038
- author: "xcode",
1039
- version: 1,
1040
687
  },
1041
- });
1042
-
1043
- await Promise.all(
1044
- [
1045
- { ratio: 1, suffix: "" },
1046
- { ratio: 2, suffix: "@2x" },
1047
- { ratio: 3, suffix: "@3x" },
1048
- ].map(({ ratio, suffix }) => {
1049
- const filePath = path.resolve(
1050
- imageSetPath,
1051
- `${logoFileName}${suffix}.png`,
1052
- );
1053
-
1054
- return logo
1055
- .clone()
1056
- .resize(logoWidth * ratio)
1057
- .png({ quality: 100 })
1058
- .toFile(filePath)
1059
- .then(({ width, height }) => {
1060
- log.write(filePath, { width, height });
1061
- });
1062
- }),
1063
- );
688
+ ],
689
+ info: {
690
+ author: "xcode",
691
+ version: 1,
692
+ },
693
+ });
1064
694
 
1065
- if (!isExpo) {
1066
- const infoPlistPath = path.resolve(iosOutputPath, "Info.plist");
695
+ const logoFileName = `logo-${fileNameSuffix}`;
1067
696
 
1068
- const infoPlist = plist.parse(hfs.text(infoPlistPath)) as Record<
1069
- string,
1070
- unknown
1071
- >;
697
+ const imagesSetPath = path.resolve(
698
+ iosOutputPath,
699
+ "Images.xcassets",
700
+ `BootSplashLogo-${fileNameSuffix}.imageset`,
701
+ );
1072
702
 
1073
- infoPlist["UILaunchStoryboardName"] = "BootSplash";
703
+ hfs.ensureDir(imagesSetPath);
1074
704
 
1075
- const formatted = formatXml(plist.build(infoPlist), {
1076
- collapseContent: true,
1077
- forceSelfClosingEmptyTag: false,
1078
- indentation: "\t",
1079
- lineSeparator: "\n",
1080
- whiteSpaceAtEndOfSelfclosingTag: false,
1081
- })
1082
- .replace(/<string\/>/gm, "<string></string>")
1083
- .replace(/^\t/gm, "");
705
+ writeJson(path.resolve(imagesSetPath, "Contents.json"), {
706
+ images: [
707
+ {
708
+ idiom: "universal",
709
+ filename: `${logoFileName}.png`,
710
+ scale: "1x",
711
+ },
712
+ {
713
+ idiom: "universal",
714
+ filename: `${logoFileName}@2x.png`,
715
+ scale: "2x",
716
+ },
717
+ {
718
+ idiom: "universal",
719
+ filename: `${logoFileName}@3x.png`,
720
+ scale: "3x",
721
+ },
722
+ ],
723
+ info: {
724
+ author: "xcode",
725
+ version: 1,
726
+ },
727
+ });
1084
728
 
1085
- hfs.write(infoPlistPath, formatted);
1086
- log.write(infoPlistPath);
729
+ await Promise.all(
730
+ [
731
+ { ratio: 1, suffix: "" },
732
+ { ratio: 2, suffix: "@2x" },
733
+ { ratio: 3, suffix: "@3x" },
734
+ ].map(({ ratio, suffix }) => {
735
+ const filePath = path.resolve(
736
+ imagesSetPath,
737
+ `${logoFileName}${suffix}.png`,
738
+ );
1087
739
 
1088
- const pbxprojectPath =
1089
- Expo.IOSConfig.Paths.getPBXProjectPath(projectRoot);
740
+ return logo.image
741
+ .clone()
742
+ .resize(logo.width * ratio)
743
+ .png({ quality: 100 })
744
+ .toFile(filePath)
745
+ .then(({ width, height }) => {
746
+ log.write(filePath, { width, height });
747
+ });
748
+ }),
749
+ );
750
+ };
1090
751
 
1091
- const xcodeProjectPath =
1092
- Expo.IOSConfig.Paths.getXcodeProjectPath(projectRoot);
752
+ const writeWebAssets = async ({
753
+ htmlTemplatePath,
754
+ props,
755
+ }: {
756
+ htmlTemplatePath: string;
757
+ props: Props;
758
+ }) => {
759
+ const { background, logo } = props;
1093
760
 
1094
- const project = Expo.IOSConfig.XcodeUtils.getPbxproj(projectRoot);
1095
- const projectName = path.basename(iosOutputPath);
1096
- const groupName = path.parse(xcodeProjectPath).name;
761
+ log.title("🌐", "Web");
1097
762
 
1098
- Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
1099
- project,
1100
- filepath: path.join(projectName, "BootSplash.storyboard"),
1101
- groupName,
1102
- isBuildFile: true,
1103
- });
763
+ const htmlTemplate = readXmlLike(htmlTemplatePath);
764
+ const { format } = await logo.image.metadata();
765
+ const prevStyle = htmlTemplate.root.querySelector("#bootsplash-style");
1104
766
 
1105
- Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
1106
- project,
1107
- filepath: path.join(projectName, "Colors.xcassets"),
1108
- groupName,
1109
- isBuildFile: true,
1110
- });
767
+ const base64 = (
768
+ format === "svg"
769
+ ? hfs.buffer(logo.path)
770
+ : await logo.image
771
+ .clone()
772
+ .resize(Math.round(logo.width * 2))
773
+ .png({ quality: 100 })
774
+ .toBuffer()
775
+ ).toString("base64");
776
+
777
+ const dataURI = `data:image/${format ? "svg+xml" : "png"};base64,${base64}`;
778
+
779
+ const nextStyle = parseHtml(dedent`
780
+ <style id="bootsplash-style">
781
+ #bootsplash {
782
+ position: absolute;
783
+ top: 0;
784
+ bottom: 0;
785
+ left: 0;
786
+ right: 0;
787
+ overflow: hidden;
788
+ display: flex;
789
+ justify-content: center;
790
+ align-items: center;
791
+ background-color: ${background.hex};
792
+ }
793
+ #bootsplash-logo {
794
+ content: url("${dataURI}");
795
+ width: ${logo.width}px;
796
+ height: ${logo.height}px;
797
+ }
798
+ </style>
799
+ `);
1111
800
 
1112
- hfs.write(pbxprojectPath, project.writeSync());
1113
- log.write(pbxprojectPath);
1114
- }
801
+ if (prevStyle != null) {
802
+ prevStyle.replaceWith(nextStyle);
803
+ } else {
804
+ htmlTemplate.root.querySelector("head")?.appendChild(nextStyle);
1115
805
  }
1116
806
 
1117
- if (htmlTemplatePath != null) {
1118
- log.title("🌐", "Web");
1119
-
1120
- const htmlTemplate = readXmlLike(htmlTemplatePath);
1121
- const { format } = await logo.metadata();
1122
- const prevStyle = htmlTemplate.root.querySelector("#bootsplash-style");
1123
-
1124
- const base64 = (
1125
- format === "svg"
1126
- ? hfs.buffer(logoPath)
1127
- : await logo
1128
- .clone()
1129
- .resize(Math.round(logoWidth * 2))
1130
- .png({ quality: 100 })
1131
- .toBuffer()
1132
- ).toString("base64");
1133
-
1134
- const dataURI = `data:image/${format ? "svg+xml" : "png"};base64,${base64}`;
1135
-
1136
- const nextStyle = parseHtml(dedent`
1137
- <style id="bootsplash-style">
1138
- #bootsplash {
1139
- position: absolute;
1140
- top: 0;
1141
- bottom: 0;
1142
- left: 0;
1143
- right: 0;
1144
- overflow: hidden;
1145
- display: flex;
1146
- justify-content: center;
1147
- align-items: center;
1148
- background-color: ${background.hex};
1149
- }
1150
- #bootsplash-logo {
1151
- content: url("${dataURI}");
1152
- width: ${logoWidth}px;
1153
- height: ${logoHeight}px;
1154
- }
1155
- </style>
1156
- `);
1157
-
1158
- if (prevStyle != null) {
1159
- prevStyle.replaceWith(nextStyle);
1160
- } else {
1161
- htmlTemplate.root.querySelector("head")?.appendChild(nextStyle);
1162
- }
807
+ const prevDiv = htmlTemplate.root.querySelector("#bootsplash");
1163
808
 
1164
- const prevDiv = htmlTemplate.root.querySelector("#bootsplash");
809
+ const nextDiv = parseHtml(dedent`
810
+ <div id="bootsplash">
811
+ <div id="bootsplash-logo"></div>
812
+ </div>
813
+ `);
1165
814
 
1166
- const nextDiv = parseHtml(dedent`
1167
- <div id="bootsplash">
1168
- <div id="bootsplash-logo"></div>
1169
- </div>
1170
- `);
815
+ if (prevDiv != null) {
816
+ prevDiv.replaceWith(nextDiv);
817
+ } else {
818
+ htmlTemplate.root.querySelector("body")?.appendChild(nextDiv);
819
+ }
1171
820
 
1172
- if (prevDiv != null) {
1173
- prevDiv.replaceWith(nextDiv);
1174
- } else {
1175
- htmlTemplate.root.querySelector("body")?.appendChild(nextDiv);
1176
- }
821
+ await writeXmlLike(htmlTemplatePath, htmlTemplate.root.toString(), {
822
+ ...htmlTemplate.formatOptions,
823
+ formatter: "prettier",
824
+ useCssPlugin: true,
825
+ });
826
+ };
1177
827
 
1178
- await writeXmlLike(htmlTemplatePath, htmlTemplate.root.toString(), {
1179
- ...htmlTemplate.formatOptions,
1180
- formatter: "prettier",
1181
- useCssPlugin: true,
1182
- });
1183
- }
828
+ const writeGenericAssets = async ({ props }: { props: Props }) => {
829
+ const { assetsOutputPath, background, logo } = props;
1184
830
 
1185
831
  log.title("📄", "Assets");
1186
-
1187
832
  hfs.ensureDir(assetsOutputPath);
1188
833
 
1189
834
  writeJson(path.resolve(assetsOutputPath, "manifest.json"), {
1190
835
  background: background.hex,
1191
836
  logo: {
1192
- width: logoWidth,
1193
- height: logoHeight,
837
+ width: logo.width,
838
+ height: logo.height,
1194
839
  },
1195
840
  } satisfies Manifest);
1196
841
 
@@ -1204,9 +849,9 @@ export const generate = async ({
1204
849
  ].map(({ ratio, suffix }) => {
1205
850
  const filePath = path.resolve(assetsOutputPath, `logo${suffix}.png`);
1206
851
 
1207
- return logo
852
+ return logo.image
1208
853
  .clone()
1209
- .resize(Math.round(logoWidth * ratio))
854
+ .resize(Math.round(logo.width * ratio))
1210
855
  .png({ quality: 100 })
1211
856
  .toFile(filePath)
1212
857
  .then(({ width, height }) => {
@@ -1214,37 +859,275 @@ export const generate = async ({
1214
859
  });
1215
860
  }),
1216
861
  );
862
+ };
863
+
864
+ export type AddonConfig = {
865
+ props: Props;
866
+ androidOutputPath: string | void;
867
+ iosOutputPath: string | void;
868
+ htmlTemplatePath: string | void;
869
+ };
870
+
871
+ const requireAddon = ({
872
+ executeAddon,
873
+ licenseKey,
874
+ }: Props):
875
+ | {
876
+ execute: (config: AddonConfig) => Promise<void>;
877
+
878
+ writeAndroidAssets: (_: {
879
+ androidOutputPath: string;
880
+ props: Props;
881
+ }) => Promise<void>;
1217
882
 
883
+ writeIOSAssets: (_: {
884
+ iosOutputPath: string;
885
+ props: Props;
886
+ }) => Promise<void>;
887
+
888
+ writeWebAssets: (_: {
889
+ htmlTemplatePath: string;
890
+ props: Props;
891
+ }) => Promise<void>;
892
+
893
+ writeGenericAssets: (_: { props: Props }) => Promise<void>;
894
+
895
+ withAndroidColorsNight: (_: {
896
+ config: Expo.ExportedConfigWithProps;
897
+ props: Props;
898
+ }) => Promise<Expo.ExportedConfigWithProps>;
899
+ }
900
+ | undefined => {
1218
901
  if (licenseKey != null && executeAddon) {
1219
- const addon = requireAddon();
902
+ try {
903
+ return require("./addon");
904
+ } catch {
905
+ return;
906
+ }
907
+ }
908
+ };
909
+
910
+ export const generate = async ({
911
+ platforms,
912
+ html,
913
+ flavor,
914
+ plist,
915
+ ...rawProps
916
+ }: {
917
+ platforms: Platforms;
918
+ html: string;
919
+ flavor: string;
920
+ plist?: string;
921
+
922
+ logo: string;
923
+ background: string;
924
+ logoWidth: number;
925
+ assetsOutput: string;
926
+ licenseKey?: string;
927
+ brand?: string;
928
+ brandWidth: number;
929
+ darkBackground?: string;
930
+ darkLogo?: string;
931
+ darkBrand?: string;
932
+ }) => {
933
+ const props = await transformProps(workingPath, rawProps);
934
+ const addon = requireAddon(props);
935
+
936
+ const { background, brand } = props;
1220
937
 
1221
- await addon?.execute({
1222
- licenseKey,
1223
- isExpo,
1224
- fileNameSuffix,
938
+ const androidOutputPath = getAndroidOutputPath({ flavor, platforms });
939
+ const iosOutputPath = getIOSOutputPath({ platforms });
940
+ const htmlTemplatePath = getHtmlTemplatePath({ html, platforms });
1225
941
 
942
+ if (androidOutputPath != null) {
943
+ await writeAndroidAssets({ androidOutputPath, props });
944
+
945
+ const manifestXmlPath = path.resolve(
1226
946
  androidOutputPath,
1227
- iosOutputPath,
1228
- htmlTemplatePath,
1229
- assetsOutputPath,
947
+ "..",
948
+ "AndroidManifest.xml",
949
+ );
1230
950
 
1231
- logoHeight,
1232
- logoWidth,
1233
- brandHeight,
1234
- brandWidth,
951
+ if (hfs.exists(manifestXmlPath)) {
952
+ const manifestXml = readXmlLike(manifestXmlPath);
953
+ const activities = manifestXml.root.querySelectorAll("activity");
1235
954
 
1236
- logoPath,
1237
- darkLogoPath,
1238
- brandPath,
1239
- darkBrandPath,
955
+ for (const activity of activities) {
956
+ if (activity.getAttribute("android:name") === ".MainActivity") {
957
+ activity.setAttribute("android:theme", "@style/BootTheme");
958
+ }
959
+ }
1240
960
 
1241
- background,
1242
- logo,
1243
- brand,
961
+ await writeXmlLike(manifestXmlPath, manifestXml.root.toString(), {
962
+ ...manifestXml.formatOptions,
963
+ formatter: "prettier",
964
+ htmlWhitespaceSensitivity: "ignore",
965
+ selfClosingTags: true,
966
+ singleAttributePerLine: true,
967
+ });
968
+ } else {
969
+ log.warn("No AndroidManifest.xml found");
970
+ }
971
+
972
+ const valuesPath = path.resolve(androidOutputPath, "values");
973
+ hfs.ensureDir(valuesPath);
974
+
975
+ const colorsXmlPath = path.resolve(valuesPath, "colors.xml");
976
+ const colorsXmlEntry = `<color name="bootsplash_background">${background.hex}</color>`;
1244
977
 
1245
- darkBackground,
1246
- darkLogo,
1247
- darkBrand,
978
+ if (hfs.exists(colorsXmlPath)) {
979
+ const colorsXml = readXmlLike(colorsXmlPath);
980
+ const nextColor = parseHtml(colorsXmlEntry);
981
+
982
+ const prevColor = colorsXml.root.querySelector(
983
+ 'color[name="bootsplash_background"]',
984
+ );
985
+
986
+ if (prevColor != null) {
987
+ prevColor.replaceWith(nextColor);
988
+ } else {
989
+ colorsXml.root.querySelector("resources")?.appendChild(nextColor);
990
+ }
991
+
992
+ await writeXmlLike(colorsXmlPath, colorsXml.root.toString(), {
993
+ ...colorsXml.formatOptions,
994
+ formatter: "xmlFormatter",
995
+ });
996
+ } else {
997
+ await writeXmlLike(
998
+ colorsXmlPath,
999
+ `<resources>${colorsXmlEntry}</resources>`,
1000
+ { formatter: "xmlFormatter" },
1001
+ );
1002
+ }
1003
+
1004
+ const stylesXmlPath = path.resolve(valuesPath, "styles.xml");
1005
+
1006
+ if (hfs.exists(stylesXmlPath)) {
1007
+ const stylesXml = readXmlLike(stylesXmlPath);
1008
+ const prevStyle = stylesXml.root.querySelector('style[name="BootTheme"]');
1009
+ const parent = prevStyle?.getAttribute("parent") ?? "Theme.BootSplash";
1010
+
1011
+ const extraItems = parseHtml(
1012
+ prevStyle?.text
1013
+ .split("\n")
1014
+ .map((line) => line.trim())
1015
+ .join("") ?? "",
1016
+ )
1017
+ .childNodes.filter((node) => {
1018
+ if (!(node instanceof HTMLElement)) {
1019
+ return true;
1020
+ }
1021
+
1022
+ const name = node.getAttribute("name");
1023
+
1024
+ return (
1025
+ name !== "bootSplashBackground" &&
1026
+ name !== "bootSplashLogo" &&
1027
+ name !== "bootSplashBrand" &&
1028
+ name !== "postBootSplashTheme"
1029
+ );
1030
+ })
1031
+ .map((node) => node.toString());
1032
+
1033
+ const styleItems: string[] = [
1034
+ ...(extraItems.length > 0 ? [...extraItems, ""] : []),
1035
+
1036
+ '<item name="bootSplashBackground">@color/bootsplash_background</item>',
1037
+ '<item name="bootSplashLogo">@drawable/bootsplash_logo</item>',
1038
+
1039
+ ...(brand != null
1040
+ ? ['<item name="bootSplashBrand">@drawable/bootsplash_brand</item>']
1041
+ : []),
1042
+
1043
+ '<item name="postBootSplashTheme">@style/AppTheme</item>',
1044
+ ];
1045
+
1046
+ const nextStyle = parseHtml(dedent`
1047
+ <style name="BootTheme" parent="${parent}">
1048
+ ${styleItems.join("\n")}
1049
+ </style>
1050
+ `);
1051
+
1052
+ prevStyle?.remove(); // remove the existing style
1053
+ stylesXml.root.querySelector("resources")?.appendChild(nextStyle);
1054
+
1055
+ await writeXmlLike(stylesXmlPath, stylesXml.root.toString(), {
1056
+ ...stylesXml.formatOptions,
1057
+ formatter: "prettier",
1058
+ htmlWhitespaceSensitivity: "ignore",
1059
+ });
1060
+ } else {
1061
+ log.warn("No styles.xml found");
1062
+ }
1063
+ }
1064
+
1065
+ if (iosOutputPath != null) {
1066
+ await writeIOSAssets({ iosOutputPath, props });
1067
+
1068
+ const infoPlistPath = getInfoPlistPath({ iosOutputPath, plist });
1069
+
1070
+ if (infoPlistPath != null) {
1071
+ const infoPlist = ExpoPlist.parse(hfs.text(infoPlistPath)) as Record<
1072
+ string,
1073
+ unknown
1074
+ >;
1075
+
1076
+ infoPlist["UILaunchStoryboardName"] = "BootSplash";
1077
+
1078
+ const formatted = formatXml(ExpoPlist.build(infoPlist), {
1079
+ collapseContent: true,
1080
+ forceSelfClosingEmptyTag: false,
1081
+ indentation: "\t",
1082
+ lineSeparator: "\n",
1083
+ whiteSpaceAtEndOfSelfclosingTag: false,
1084
+ })
1085
+ .replace(/<string\/>/gm, "<string></string>")
1086
+ .replace(/^\t/gm, "");
1087
+
1088
+ hfs.write(infoPlistPath, formatted);
1089
+ log.write(infoPlistPath);
1090
+ }
1091
+
1092
+ const pbxprojectPath = Expo.IOSConfig.Paths.getPBXProjectPath(projectRoot);
1093
+
1094
+ const xcodeProjectPath =
1095
+ Expo.IOSConfig.Paths.getXcodeProjectPath(projectRoot);
1096
+
1097
+ const project = Expo.IOSConfig.XcodeUtils.getPbxproj(projectRoot);
1098
+ const projectName = path.basename(iosOutputPath);
1099
+ const groupName = path.parse(xcodeProjectPath).name;
1100
+
1101
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
1102
+ project,
1103
+ filepath: path.join(projectName, "BootSplash.storyboard"),
1104
+ groupName,
1105
+ isBuildFile: true,
1106
+ });
1107
+
1108
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
1109
+ project,
1110
+ filepath: path.join(projectName, "Colors.xcassets"),
1111
+ groupName,
1112
+ isBuildFile: true,
1113
+ });
1114
+
1115
+ hfs.write(pbxprojectPath, project.writeSync());
1116
+ log.write(pbxprojectPath);
1117
+ }
1118
+
1119
+ if (htmlTemplatePath != null) {
1120
+ await writeWebAssets({ htmlTemplatePath, props });
1121
+ }
1122
+
1123
+ await writeGenericAssets({ props });
1124
+
1125
+ if (addon != null) {
1126
+ await addon.execute({
1127
+ props,
1128
+ androidOutputPath,
1129
+ iosOutputPath,
1130
+ htmlTemplatePath,
1248
1131
  });
1249
1132
  } else {
1250
1133
  console.log(`
@@ -1262,3 +1145,360 @@ ${pc.blue("┗━━━━━━━━━━━━━━━━━━━━━━
1262
1145
  `\n💖 Thanks for using ${pc.underline("react-native-bootsplash")}`,
1263
1146
  );
1264
1147
  };
1148
+
1149
+ // Expo plugin
1150
+
1151
+ const withoutExpoSplashScreen: Expo.ConfigPlugin<RawProps> =
1152
+ Expo.createRunOncePlugin(
1153
+ (expoConfig) => expoConfig,
1154
+ "expo-splash-screen",
1155
+ "skip",
1156
+ );
1157
+
1158
+ const withAndroidAssets: Expo.ConfigPlugin<RawProps> = (expoConfig, rawProps) =>
1159
+ Expo.withDangerousMod(expoConfig, [
1160
+ "android",
1161
+ async (config) => {
1162
+ const { platformProjectRoot, projectRoot } = config.modRequest;
1163
+ const props = await transformProps(projectRoot, rawProps);
1164
+ const addon = requireAddon(props);
1165
+
1166
+ const androidOutputPath = path.resolve(
1167
+ platformProjectRoot,
1168
+ "app",
1169
+ "src",
1170
+ "main",
1171
+ "res",
1172
+ );
1173
+
1174
+ await writeAndroidAssets({ androidOutputPath, props });
1175
+ await addon?.writeAndroidAssets({ androidOutputPath, props });
1176
+
1177
+ return config;
1178
+ },
1179
+ ]);
1180
+
1181
+ const withAndroidManifest: Expo.ConfigPlugin<RawProps> = (expoConfig) =>
1182
+ Expo.withAndroidManifest(expoConfig, (config) => {
1183
+ config.modResults.manifest.application?.forEach((application) => {
1184
+ if (application.$["android:name"] === ".MainApplication") {
1185
+ const { activity } = application;
1186
+
1187
+ activity?.forEach((activity) => {
1188
+ if (activity.$["android:name"] === ".MainActivity") {
1189
+ activity.$["android:theme"] = "@style/BootTheme";
1190
+ }
1191
+ });
1192
+ }
1193
+ });
1194
+
1195
+ return config;
1196
+ });
1197
+
1198
+ const withMainActivity: Expo.ConfigPlugin<RawProps> = (expoConfig) =>
1199
+ Expo.withMainActivity(expoConfig, (config) => {
1200
+ const { modResults } = config;
1201
+
1202
+ const withImports = addImports(
1203
+ modResults.contents.replace(
1204
+ /(\/\/ )?setTheme\(R\.style\.AppTheme\)/,
1205
+ "// setTheme(R.style.AppTheme)",
1206
+ ),
1207
+ ["android.os.Bundle", "com.zoontek.rnbootsplash.RNBootSplash"],
1208
+ false,
1209
+ );
1210
+
1211
+ // indented with 4 spaces
1212
+ const withInit = mergeContents({
1213
+ src: withImports,
1214
+ comment: " //",
1215
+ tag: "bootsplash-init",
1216
+ offset: 0,
1217
+ anchor: /super\.onCreate\(null\)/,
1218
+ newSrc: " RNBootSplash.init(this, R.style.BootTheme)",
1219
+ });
1220
+
1221
+ return {
1222
+ ...config,
1223
+ modResults: {
1224
+ ...modResults,
1225
+ contents: withInit.contents,
1226
+ },
1227
+ };
1228
+ });
1229
+
1230
+ const withAndroidStyles: Expo.ConfigPlugin<RawProps> = (expoConfig, rawProps) =>
1231
+ Expo.withAndroidStyles(expoConfig, async (config) => {
1232
+ const { projectRoot } = config.modRequest;
1233
+ const { android, brand } = await transformProps(projectRoot, rawProps);
1234
+ const { darkContentBarsStyle } = android;
1235
+
1236
+ const { modResults } = config;
1237
+ const { resources } = modResults;
1238
+ const { style = [] } = resources;
1239
+
1240
+ const item = [
1241
+ {
1242
+ $: { name: "postBootSplashTheme" },
1243
+ _: "@style/AppTheme",
1244
+ },
1245
+ {
1246
+ $: { name: "bootSplashBackground" },
1247
+ _: "@color/bootsplash_background",
1248
+ },
1249
+ {
1250
+ $: { name: "bootSplashLogo" },
1251
+ _: "@drawable/bootsplash_logo",
1252
+ },
1253
+ ];
1254
+
1255
+ if (brand != null) {
1256
+ item.push({
1257
+ $: { name: "bootSplashBrand" },
1258
+ _: "@drawable/bootsplash_brand",
1259
+ });
1260
+ }
1261
+ if (darkContentBarsStyle != null) {
1262
+ item.push({
1263
+ $: { name: "darkContentBarsStyle" },
1264
+ _: String(darkContentBarsStyle),
1265
+ });
1266
+ }
1267
+
1268
+ const withBootTheme = [
1269
+ ...style.filter(({ $ }) => $.name !== "BootTheme"),
1270
+ {
1271
+ $: {
1272
+ name: "BootTheme",
1273
+ parent: "Theme.BootSplash",
1274
+ },
1275
+ item,
1276
+ },
1277
+ ];
1278
+
1279
+ return {
1280
+ ...config,
1281
+ modResults: {
1282
+ ...modResults,
1283
+ resources: {
1284
+ ...resources,
1285
+ style: withBootTheme,
1286
+ },
1287
+ },
1288
+ };
1289
+ });
1290
+
1291
+ const withAndroidColors: Expo.ConfigPlugin<RawProps> = (expoConfig, rawProps) =>
1292
+ Expo.withAndroidColors(expoConfig, async (config) => {
1293
+ const { projectRoot } = config.modRequest;
1294
+ const { background } = await transformProps(projectRoot, rawProps);
1295
+
1296
+ config.modResults = assignColorValue(config.modResults, {
1297
+ name: "bootsplash_background",
1298
+ value: background.hex,
1299
+ });
1300
+
1301
+ return config;
1302
+ });
1303
+
1304
+ const withAndroidColorsNight: Expo.ConfigPlugin<RawProps> = (
1305
+ expoConfig,
1306
+ rawProps,
1307
+ ) =>
1308
+ Expo.withAndroidColorsNight(expoConfig, async (config) => {
1309
+ const { projectRoot } = config.modRequest;
1310
+ const props = await transformProps(projectRoot, rawProps);
1311
+ const addon = requireAddon(props);
1312
+
1313
+ return addon != null
1314
+ ? await addon.withAndroidColorsNight({ config, props })
1315
+ : config;
1316
+ });
1317
+
1318
+ const withIOSAssets: Expo.ConfigPlugin<RawProps> = (expoConfig, rawProps) =>
1319
+ Expo.withDangerousMod(expoConfig, [
1320
+ "ios",
1321
+ async (config) => {
1322
+ const {
1323
+ platformProjectRoot,
1324
+ projectName = "",
1325
+ projectRoot,
1326
+ } = config.modRequest;
1327
+
1328
+ const props = await transformProps(projectRoot, rawProps);
1329
+ const addon = requireAddon(props);
1330
+ const iosOutputPath = path.resolve(platformProjectRoot, projectName);
1331
+
1332
+ await writeIOSAssets({ iosOutputPath, props });
1333
+ await addon?.writeIOSAssets({ iosOutputPath, props });
1334
+
1335
+ return config;
1336
+ },
1337
+ ]);
1338
+
1339
+ const withAppDelegate: Expo.ConfigPlugin<RawProps> = (expoConfig) =>
1340
+ Expo.withAppDelegate(expoConfig, (config) => {
1341
+ const { modResults } = config;
1342
+ const { language } = modResults;
1343
+
1344
+ if (language !== "swift") {
1345
+ throw new Error(
1346
+ `Cannot modify the project AppDelegate as it's not in a supported language: ${language}`,
1347
+ );
1348
+ }
1349
+
1350
+ const withHeader = mergeContents({
1351
+ src: modResults.contents,
1352
+ comment: "//",
1353
+ tag: "bootsplash-header",
1354
+ offset: 1,
1355
+ anchor: /import Expo/,
1356
+ newSrc: "import RNBootSplash",
1357
+ });
1358
+
1359
+ const withRootView = mergeContents({
1360
+ src: withHeader.contents,
1361
+ comment: "//",
1362
+ tag: "bootsplash-init",
1363
+ offset: 1,
1364
+ anchor: /class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {/,
1365
+ newSrc: dedent`
1366
+ public override func customize(_ rootView: UIView) {
1367
+ super.customize(rootView)
1368
+ RNBootSplash.initWithStoryboard("BootSplash", rootView: rootView)
1369
+ }
1370
+ `,
1371
+ });
1372
+
1373
+ return {
1374
+ ...config,
1375
+ modResults: {
1376
+ ...modResults,
1377
+ contents: withRootView.contents,
1378
+ },
1379
+ };
1380
+ });
1381
+
1382
+ const withInfoPlist: Expo.ConfigPlugin<RawProps> = (expoConfig) =>
1383
+ Expo.withInfoPlist(expoConfig, (config) => {
1384
+ config.modResults["UILaunchStoryboardName"] = "BootSplash";
1385
+ return config;
1386
+ });
1387
+
1388
+ const withXcodeProject: Expo.ConfigPlugin<RawProps> = (expoConfig) =>
1389
+ Expo.withXcodeProject(expoConfig, (config) => {
1390
+ const { projectName = "" } = config.modRequest;
1391
+
1392
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
1393
+ filepath: path.join(projectName, "BootSplash.storyboard"),
1394
+ groupName: projectName,
1395
+ project: config.modResults,
1396
+ isBuildFile: true,
1397
+ });
1398
+
1399
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
1400
+ filepath: path.join(projectName, "Colors.xcassets"),
1401
+ groupName: projectName,
1402
+ project: config.modResults,
1403
+ isBuildFile: true,
1404
+ });
1405
+
1406
+ return config;
1407
+ });
1408
+
1409
+ const withWebAssets: Expo.ConfigPlugin<RawProps> = (expoConfig, rawProps) =>
1410
+ Expo.withDangerousMod(expoConfig, [
1411
+ expoConfig.platforms?.includes("ios") ? "ios" : "android",
1412
+ async (config) => {
1413
+ const { projectRoot } = config.modRequest;
1414
+ const props = await transformProps(projectRoot, rawProps);
1415
+ const addon = requireAddon(props);
1416
+
1417
+ const fileName = "public/index.html";
1418
+ const htmlTemplatePath = path.resolve(projectRoot, fileName);
1419
+
1420
+ if (!hfs.exists(htmlTemplatePath)) {
1421
+ await exec(`npx expo customize ${fileName}`);
1422
+ }
1423
+
1424
+ await writeWebAssets({ htmlTemplatePath, props });
1425
+ await addon?.writeWebAssets({ htmlTemplatePath, props });
1426
+
1427
+ return config;
1428
+ },
1429
+ ]);
1430
+
1431
+ const withGenericAssets: Expo.ConfigPlugin<RawProps> = (expoConfig, rawProps) =>
1432
+ Expo.withDangerousMod(expoConfig, [
1433
+ expoConfig.platforms?.includes("ios") ? "ios" : "android",
1434
+ async (config) => {
1435
+ const { projectRoot } = config.modRequest;
1436
+ const props = await transformProps(projectRoot, rawProps);
1437
+ const addon = requireAddon(props);
1438
+
1439
+ await writeGenericAssets({ props });
1440
+ await addon?.writeGenericAssets({ props });
1441
+
1442
+ return config;
1443
+ },
1444
+ ]);
1445
+
1446
+ export const withBootSplash = Expo.createRunOncePlugin<
1447
+ (Partial<RawProps> & { logo: string }) | undefined
1448
+ >((config, baseProps) => {
1449
+ const { platforms = [], sdkVersion = "0.1.0" } = config;
1450
+
1451
+ isExpo = true;
1452
+
1453
+ if (semver.lt(sdkVersion, "54.0.0")) {
1454
+ log.error("Requires Expo 54.0.0 (or higher)");
1455
+ process.exit(1);
1456
+ }
1457
+
1458
+ if (!baseProps?.logo) {
1459
+ log.error("Missing required parameter 'logo'");
1460
+ process.exit(1);
1461
+ }
1462
+
1463
+ const rawProps: RawProps = {
1464
+ assetsOutput: "assets/bootsplash",
1465
+ background: "#fff",
1466
+ brandWidth: 80,
1467
+ logoWidth: 100,
1468
+ ...baseProps,
1469
+ };
1470
+
1471
+ const plugins: Expo.ConfigPlugin<RawProps>[] = [
1472
+ withoutExpoSplashScreen,
1473
+ withGenericAssets,
1474
+ ];
1475
+
1476
+ if (platforms.includes("android")) {
1477
+ plugins.push(
1478
+ withAndroidAssets,
1479
+ withAndroidManifest,
1480
+ withMainActivity,
1481
+ withAndroidStyles,
1482
+ withAndroidColors,
1483
+ withAndroidColorsNight,
1484
+ );
1485
+ }
1486
+
1487
+ if (platforms.includes("ios")) {
1488
+ plugins.push(
1489
+ withIOSAssets,
1490
+ withAppDelegate,
1491
+ withInfoPlist,
1492
+ withXcodeProject,
1493
+ );
1494
+ }
1495
+
1496
+ if (platforms.includes("web")) {
1497
+ plugins.push(withWebAssets);
1498
+ }
1499
+
1500
+ return Expo.withPlugins(
1501
+ config,
1502
+ plugins.map((plugin) => [plugin, rawProps]),
1503
+ );
1504
+ }, PACKAGE_NAME);