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