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
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
 
3
3
  import * as Expo from "@expo/config-plugins";
4
- import plist from "@expo/plist";
4
+ import { assignColorValue } from "@expo/config-plugins/build/android/Colors";
5
+ import { addImports } from "@expo/config-plugins/build/android/codeMod";
6
+ import { mergeContents } from "@expo/config-plugins/build/utils/generateCode";
7
+ import ExpoPlist from "@expo/plist";
5
8
  import { projectConfig as getAndroidProjectConfig } from "@react-native-community/cli-config-android";
6
9
  import { getProjectConfig as getAppleProjectConfig } from "@react-native-community/cli-config-apple";
7
10
  import { findProjectRoot } from "@react-native-community/cli-tools";
@@ -20,13 +23,10 @@ import sharp from "sharp";
20
23
  import { dedent } from "ts-dedent";
21
24
  import util from "util";
22
25
  import formatXml from "xml-formatter";
26
+ const PACKAGE_NAME = "react-native-bootsplash";
27
+ let isExpo = false;
23
28
  const workingPath = process.env.INIT_CWD ?? process.env.PWD ?? process.cwd();
24
29
  const projectRoot = findProjectRoot(workingPath);
25
- const getIOSProjectConfig = getAppleProjectConfig({
26
- platformName: "ios"
27
- });
28
- const ios = getIOSProjectConfig(projectRoot, {});
29
- const android = getAndroidProjectConfig(projectRoot);
30
30
  const promisifiedExec = util.promisify(childProcess.exec);
31
31
  const exec = cmd => promisifiedExec(cmd).then(({
32
32
  stdout,
@@ -34,16 +34,20 @@ const exec = cmd => promisifiedExec(cmd).then(({
34
34
  }) => stdout || stderr);
35
35
  export const log = {
36
36
  error: text => {
37
- console.log(pc.red(`❌ ${text}`));
37
+ console.log(pc.red(isExpo ? `❌ [${PACKAGE_NAME}] ${text}` : `❌ ${text}`));
38
38
  },
39
39
  title: (emoji, text) => {
40
- console.log(`\n${emoji} ${pc.underline(pc.bold(text))}`);
40
+ if (!isExpo) {
41
+ console.log(`\n${emoji} ${pc.underline(pc.bold(text))}`);
42
+ }
41
43
  },
42
44
  warn: text => {
43
- console.log(pc.yellow(`⚠️ ${text}`));
45
+ console.log(pc.yellow(isExpo ? `⚠️ [${PACKAGE_NAME}] ${text}` : `⚠️ ${text}`));
44
46
  },
45
47
  write: (filePath, dimensions) => {
46
- console.log(` ${path.relative(workingPath, filePath)}` + (dimensions != null ? ` (${dimensions.width}x${dimensions.height})` : ""));
48
+ if (!isExpo) {
49
+ console.log(` ${path.relative(workingPath, filePath)}` + (dimensions != null ? ` (${dimensions.width}x${dimensions.height})` : ""));
50
+ }
47
51
  }
48
52
  };
49
53
  const parseColor = value => {
@@ -63,20 +67,19 @@ const parseColor = value => {
63
67
  rgb
64
68
  };
65
69
  };
66
- const getStoryboard = ({
67
- logoHeight,
68
- logoWidth,
69
- background: {
70
+ const getStoryboard = props => {
71
+ const {
72
+ background,
73
+ logo,
74
+ fileNameSuffix
75
+ } = props;
76
+ const {
70
77
  R,
71
78
  G,
72
79
  B
73
- },
74
- fileNameSuffix
75
- }) => {
80
+ } = background.rgb;
76
81
  const frameWidth = 375;
77
82
  const frameHeight = 667;
78
- const logoX = (frameWidth - logoWidth) / 2;
79
- const logoY = (frameHeight - logoHeight) / 2;
80
83
  return dedent`
81
84
  <?xml version="1.0" encoding="UTF-8"?>
82
85
  <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
@@ -98,7 +101,7 @@ const getStoryboard = ({
98
101
  <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
99
102
  <subviews>
100
103
  <imageView autoresizesSubviews="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="BootSplashLogo-${fileNameSuffix}" translatesAutoresizingMaskIntoConstraints="NO" id="3lX-Ut-9ad">
101
- <rect key="frame" x="${logoX}" y="${logoY}" width="${logoWidth}" height="${logoHeight}"/>
104
+ <rect key="frame" x="${(frameWidth - logo.width) / 2}" y="${(frameHeight - logo.height) / 2}" width="${logo.width}" height="${logo.height}"/>
102
105
  <accessibility key="accessibilityConfiguration">
103
106
  <accessibilityTraits key="traits" image="YES" notEnabled="YES"/>
104
107
  </accessibility>
@@ -118,7 +121,7 @@ const getStoryboard = ({
118
121
  </scene>
119
122
  </scenes>
120
123
  <resources>
121
- <image name="BootSplashLogo-${fileNameSuffix}" width="${logoWidth}" height="${logoHeight}"/>
124
+ <image name="BootSplashLogo-${fileNameSuffix}" width="${logo.width}" height="${logo.height}"/>
122
125
  <namedColor name="BootSplashBackground-${fileNameSuffix}">
123
126
  <color red="${R}" green="${G}" blue="${B}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
124
127
  </namedColor>
@@ -140,20 +143,6 @@ export const hfs = {
140
143
  recursive: true
141
144
  }),
142
145
  text: path => fs.readFileSync(path, "utf-8"),
143
- copy: (src, dest) => {
144
- if (hfs.isDir(src) || !hfs.exists(dest)) {
145
- return fs.copySync(src, dest, {
146
- overwrite: true
147
- });
148
- }
149
- const srcBuffer = fs.readFileSync(src);
150
- const destBuffer = fs.readFileSync(dest);
151
- if (!srcBuffer.equals(destBuffer)) {
152
- return fs.copySync(src, dest, {
153
- overwrite: true
154
- });
155
- }
156
- },
157
146
  ensureDir: dir => {
158
147
  fs.mkdirSync(dir, {
159
148
  recursive: true
@@ -164,52 +153,6 @@ export const hfs = {
164
153
  fs.writeFileSync(path, trimmed === "" ? trimmed : trimmed + "\n", "utf-8");
165
154
  }
166
155
  };
167
-
168
- // Adapted from https://github.com/square/find-yarn-workspace-root
169
- const findUp = (from, matcher) => {
170
- let previous;
171
- let current = path.normalize(from);
172
- do {
173
- const found = matcher(current);
174
- if (typeof found !== "undefined") {
175
- return found;
176
- }
177
- previous = current;
178
- current = path.dirname(current);
179
- } while (current !== previous);
180
- };
181
- export const getExpoConfig = from => {
182
- const hasDependency = findUp(from, dir => {
183
- const pkgPath = path.resolve(dir, "package.json");
184
- if (fs.existsSync(pkgPath)) {
185
- try {
186
- const pkg = hfs.json(pkgPath);
187
- return pkg.dependencies?.expo != null;
188
- } catch {} // eslint-disable-line no-empty
189
- }
190
- }) ?? false;
191
- if (!hasDependency) {
192
- return {
193
- isExpo: false
194
- };
195
- }
196
- const version = findUp(from, dir => {
197
- const pkgPath = path.resolve(dir, "node_modules", "expo", "package.json");
198
- if (fs.existsSync(pkgPath)) {
199
- try {
200
- const pkg = hfs.json(pkgPath);
201
- return pkg.version;
202
- } catch {} // eslint-disable-line no-empty
203
- }
204
- });
205
- if (version == null || semver.lt(version, "51.0.20")) {
206
- log.error("Requires Expo 51.0.20 (or higher)");
207
- process.exit(1);
208
- }
209
- return {
210
- isExpo: true
211
- };
212
- };
213
156
  export const writeJson = (filePath, content) => {
214
157
  hfs.write(filePath, JSON.stringify(content, null, 2));
215
158
  log.write(filePath);
@@ -262,105 +205,27 @@ export const writeXmlLike = async (filePath, content, {
262
205
  log.write(filePath);
263
206
  }
264
207
  };
265
- export const cleanIOSAssets = dir => {
266
- hfs.readDir(dir).filter(file => file === "Colors.xcassets" || file === "Images.xcassets").map(file => path.join(dir, file)).flatMap(dir => hfs.readDir(dir).filter(file => file.startsWith("BootSplash")).map(file => path.join(dir, file))).forEach(file => {
267
- hfs.rm(file);
268
- });
269
- };
270
- const getImageBase64 = async (image, width) => {
271
- if (image == null) {
272
- return "";
273
- }
274
- const buffer = await image.clone().resize(width).png({
275
- quality: 100
276
- }).toBuffer();
277
- return buffer.toString("base64");
278
- };
279
- const getFileNameSuffix = async ({
280
- background,
281
- brand,
282
- brandWidth,
283
- darkBackground,
284
- darkBrand,
285
- darkLogo,
286
- logo,
287
- logoWidth
288
- }) => {
289
- const [logoHash, darkLogoHash, brandHash, darkBrandHash] = await Promise.all([getImageBase64(logo, logoWidth), getImageBase64(darkLogo, logoWidth), getImageBase64(brand, brandWidth), getImageBase64(darkBrand, brandWidth)]);
290
- const record = {
291
- background: background.hex,
292
- darkBackground: darkBackground?.hex ?? "",
293
- logo: logoHash,
294
- darkLogo: darkLogoHash,
295
- brand: brandHash,
296
- darkBrand: darkBrandHash
297
- };
298
- const stableKey = Object.keys(record).sort().map(key => record[key]).join();
299
- return crypto.createHash("shake256", {
300
- outputLength: 3
301
- }).update(stableKey).digest("hex").toLowerCase();
302
- };
303
- const ensureSupportedFormat = async (name, image) => {
304
- if (image == null) {
305
- return;
306
- }
307
- const {
308
- format
309
- } = await image.metadata();
310
- if (format !== "png" && format !== "svg") {
311
- log.error(`${name} image file format (${format}) is not supported`);
312
- process.exit(1);
313
- }
314
- };
315
208
  const getAndroidOutputPath = ({
316
- assetsOutputPath,
317
- brandHeight,
318
- brandWidth,
319
209
  flavor,
320
- isExpo,
321
- logoHeight,
322
- logoWidth,
323
210
  platforms
324
211
  }) => {
325
- if (!platforms.includes("android")) {
326
- return;
327
- }
328
- const withSizeChecks = assetsOutputPath => {
329
- if (logoWidth > 192 || logoHeight > 192) {
330
- return log.warn("Logo size exceeding 192x192dp will be cropped by Android. Skipping Android assets generation…");
331
- }
332
- if (brandWidth > 200 || brandHeight > 80) {
333
- return log.warn("Brand size exceeding 200x80dp will be cropped by Android. Skipping Android assets generation…");
334
- }
335
- if (logoWidth > 134 || logoHeight > 134) {
336
- log.warn("Logo size exceeds 134x134dp. It might be cropped by Android.");
337
- }
338
- return assetsOutputPath;
339
- };
340
- if (isExpo) {
341
- return withSizeChecks(path.resolve(assetsOutputPath, "android"));
342
- }
343
- if (android == null) {
212
+ const android = getAndroidProjectConfig(projectRoot);
213
+ if (!platforms.includes("android") || android == null) {
344
214
  return;
345
215
  }
346
216
  const androidOutputPath = path.resolve(android.sourceDir, android.appName, "src", flavor, "res");
347
217
  if (!hfs.exists(androidOutputPath)) {
348
218
  return log.warn(`No ${path.relative(workingPath, androidOutputPath)} directory found. Skipping Android assets generation…`);
349
219
  }
350
- return withSizeChecks(androidOutputPath);
220
+ return androidOutputPath;
351
221
  };
352
222
  const getIOSOutputPath = ({
353
- assetsOutputPath,
354
- isExpo,
355
223
  platforms
356
224
  }) => {
357
- if (!platforms.includes("ios")) {
358
- return;
359
- }
360
- if (isExpo) {
361
- return path.resolve(assetsOutputPath, "ios");
362
- }
363
- if (ios == null) {
225
+ const ios = getAppleProjectConfig({
226
+ platformName: "ios"
227
+ })(projectRoot, {});
228
+ if (!platforms.includes("ios") || ios == null) {
364
229
  return;
365
230
  }
366
231
  if (ios.xcodeProject == null) {
@@ -372,428 +237,361 @@ const getIOSOutputPath = ({
372
237
  }
373
238
  return iosOutputPath;
374
239
  };
375
- const getHtmlTemplatePath = async ({
376
- isExpo,
240
+ const getHtmlTemplatePath = ({
377
241
  html,
378
242
  platforms
379
243
  }) => {
380
244
  if (!platforms.includes("web")) {
381
245
  return;
382
246
  }
383
- if (isExpo) {
384
- const htmlTemplatePath = path.resolve(workingPath, html);
385
- const htmlTemplateRelativePath = path.relative(workingPath, htmlTemplatePath);
386
- if (htmlTemplateRelativePath === "public/index.html" && !hfs.exists(htmlTemplatePath)) {
387
- const cmd = `npx expo customize ${htmlTemplateRelativePath}`;
388
- console.log(pc.dim(`Running ${cmd}`));
389
- await exec(cmd);
390
- }
391
- }
392
247
  const htmlTemplatePath = path.resolve(workingPath, html);
393
248
  if (!hfs.exists(htmlTemplatePath)) {
394
- return log.warn(`No ${path.relative(workingPath, htmlTemplatePath)} file found. Skipping HTML + CSS generation…`);
249
+ return log.warn(`No ${path.relative(workingPath, htmlTemplatePath)} found. Skipping HTML + CSS generation…`);
395
250
  }
396
251
  return htmlTemplatePath;
397
252
  };
398
- const getImageHeight = (image, width) => {
399
- if (image == null) {
400
- return Promise.resolve(0);
253
+ const getInfoPlistPath = ({
254
+ iosOutputPath,
255
+ plist
256
+ }) => {
257
+ if (plist != null) {
258
+ const infoPlistPath = path.resolve(workingPath, plist);
259
+ if (!hfs.exists(infoPlistPath)) {
260
+ return log.warn(`No ${path.relative(workingPath, infoPlistPath)} found`);
261
+ }
262
+ return infoPlistPath;
401
263
  }
402
- return image.clone().resize(width).toBuffer().then(buffer => sharp(buffer).metadata()).then(({
403
- height = 0
404
- }) => Math.round(height));
264
+ return path.resolve(iosOutputPath, "Info.plist");
405
265
  };
406
- const requireAddon = () => {
407
- try {
408
- return require("./addon"); // eslint-disable-line
409
- } catch {
410
- return;
266
+ const toAsset = async (filePath, width) => {
267
+ const image = sharp(filePath);
268
+ const {
269
+ format
270
+ } = await image.metadata();
271
+ if (format !== "png" && format !== "svg") {
272
+ log.error(`${path.basename(filePath)} image file format (${format}) is not supported`);
273
+ process.exit(1);
411
274
  }
275
+ const [height, hash] = await Promise.all([image.clone().resize(width).toBuffer().then(buffer => sharp(buffer).metadata()).then(({
276
+ height = 0
277
+ }) => Math.round(height)), image.clone().resize(width).png({
278
+ quality: 100
279
+ }).toBuffer().then(buffer => buffer.toString("base64"))]);
280
+ return {
281
+ path: filePath,
282
+ image,
283
+ hash,
284
+ height,
285
+ width
286
+ };
412
287
  };
413
- export const generate = async ({
414
- projectType,
415
- platforms,
416
- html,
417
- flavor,
288
+ const transformProps = async (rootPath, {
289
+ android = {},
418
290
  licenseKey,
419
- ...args
291
+ ...rawProps
420
292
  }) => {
421
- const isExpo = projectType === "expo" || projectType === "detect" && getExpoConfig(workingPath).isExpo;
422
- if (semver.lt(process.versions.node, "18.0.0")) {
423
- log.error("Requires Node 18 (or higher)");
293
+ if (semver.lt(process.versions.node, "20.0.0")) {
294
+ log.error("Requires Node 20 (or higher)");
424
295
  process.exit(1);
425
296
  }
426
- const logoPath = path.resolve(workingPath, args.logo);
427
- const darkLogoPath = args.darkLogo != null ? path.resolve(workingPath, args.darkLogo) : undefined;
428
- const brandPath = args.brand != null ? path.resolve(workingPath, args.brand) : undefined;
429
- const darkBrandPath = args.darkBrand != null ? path.resolve(workingPath, args.darkBrand) : undefined;
430
- const assetsOutputPath = path.resolve(workingPath, args.assetsOutput);
431
- const logo = sharp(logoPath);
432
- const darkLogo = darkLogoPath != null ? sharp(darkLogoPath) : undefined;
433
- const brand = brandPath != null ? sharp(brandPath) : undefined;
434
- const darkBrand = darkBrandPath != null ? sharp(darkBrandPath) : undefined;
435
- const background = parseColor(args.background);
436
- const logoWidth = args.logoWidth - args.logoWidth % 2;
437
- const brandWidth = args.brandWidth - args.brandWidth % 2;
438
- const darkBackground = args.darkBackground != null ? parseColor(args.darkBackground) : undefined;
297
+ const assetsOutputPath = path.resolve(rootPath, rawProps.assetsOutput);
298
+ const logoPath = path.resolve(rootPath, rawProps.logo);
299
+ const darkLogoPath = rawProps.darkLogo != null ? path.resolve(rootPath, rawProps.darkLogo) : undefined;
300
+ const brandPath = rawProps.brand != null ? path.resolve(rootPath, rawProps.brand) : undefined;
301
+ const darkBrandPath = rawProps.darkBrand != null ? path.resolve(rootPath, rawProps.darkBrand) : undefined;
302
+ const logoWidth = rawProps.logoWidth - rawProps.logoWidth % 2;
303
+ const brandWidth = rawProps.brandWidth - rawProps.brandWidth % 2;
304
+ const [logo, darkLogo, brand, darkBrand] = await Promise.all([toAsset(logoPath, logoWidth), darkLogoPath != null ? toAsset(darkLogoPath, logoWidth) : undefined, brandPath != null ? toAsset(brandPath, brandWidth) : undefined, darkBrandPath != null ? toAsset(darkBrandPath, brandWidth) : undefined]);
305
+ const background = parseColor(rawProps.background);
306
+ const darkBackground = rawProps.darkBackground != null ? parseColor(rawProps.darkBackground) : undefined;
439
307
  const executeAddon = brand != null || darkBackground != null || darkLogo != null || darkBrand != null;
440
308
  if (licenseKey != null && !executeAddon) {
441
309
  log.warn("You specified a license key but none of the options that requires it.");
442
310
  }
311
+ const optionNames = {
312
+ brand: isExpo ? "brand" : "--brand",
313
+ darkBackground: isExpo ? "darkBackground" : "--dark-background",
314
+ darkLogo: isExpo ? "darkLogo" : "--dark-logo",
315
+ darkBrand: isExpo ? "darkBrand" : "--dark-brand"
316
+ };
443
317
  if (licenseKey == null && executeAddon) {
444
- const options = [brand != null ? "brand" : "", darkBackground != null ? "dark-background" : "", darkLogo != null ? "dark-logo" : "", darkBrand != null ? "dark-brand" : ""].filter(option => option !== "").map(option => `--${option}`).join(", ");
318
+ const options = [brand != null ? optionNames.brand : "", darkBackground != null ? optionNames.darkBackground : "", darkLogo != null ? optionNames.darkLogo : "", darkBrand != null ? optionNames.darkBrand : ""].filter(option => option !== "").join(", ");
445
319
  log.error(`You need to specify a license key in order to use ${options}.`);
446
320
  process.exit(1);
447
321
  }
448
322
  if (brand == null && darkBrand != null) {
449
- log.error("--dark-brand option couldn't be used without --brand.");
323
+ log.error(`${optionNames.darkBrand} option couldn't be used without ${optionNames.brand}.`);
450
324
  process.exit(1);
451
325
  }
452
- await ensureSupportedFormat("Logo", logo);
453
- await ensureSupportedFormat("Dark logo", darkLogo);
454
- await ensureSupportedFormat("Brand", brand);
455
- await ensureSupportedFormat("Dark brand", darkBrand);
456
- const logoHeight = await getImageHeight(logo, logoWidth);
457
- const brandHeight = await getImageHeight(brand, brandWidth);
458
- if (logoWidth < args.logoWidth) {
326
+ if (logoWidth < rawProps.logoWidth) {
459
327
  log.warn(`Logo width must be a multiple of 2. It has been rounded to ${logoWidth}dp.`);
460
328
  }
461
- if (brandWidth < args.brandWidth) {
329
+ if (brandWidth < rawProps.brandWidth) {
462
330
  log.warn(`Brand width must be a multiple of 2. It has been rounded to ${brandWidth}dp.`);
463
331
  }
464
- const fileNameSuffix = await getFileNameSuffix({
332
+ const record = {
333
+ background: background.hex,
334
+ darkBackground: darkBackground?.hex ?? "",
335
+ logo: logo.hash,
336
+ darkLogo: darkLogo?.hash ?? "",
337
+ brand: brand?.hash ?? "",
338
+ darkBrand: darkBrand?.hash ?? ""
339
+ };
340
+ const stableKey = Object.keys(record).sort().map(key => record[key]).join();
341
+ const fileNameSuffix = crypto.createHash("shake256", {
342
+ outputLength: 3
343
+ }).update(stableKey).digest("hex").toLowerCase();
344
+ return {
345
+ android,
346
+ assetsOutputPath,
347
+ licenseKey,
348
+ executeAddon,
465
349
  background,
466
- brand,
467
- brandWidth,
468
350
  darkBackground,
469
- darkBrand,
351
+ logo,
470
352
  darkLogo,
353
+ brand,
354
+ darkBrand,
355
+ fileNameSuffix
356
+ };
357
+ };
358
+ const writeAndroidAssets = async ({
359
+ androidOutputPath,
360
+ props
361
+ }) => {
362
+ const {
471
363
  logo,
472
- logoWidth
473
- });
474
- const androidOutputPath = getAndroidOutputPath({
475
- assetsOutputPath,
476
- brandHeight,
477
- brandWidth,
478
- flavor,
479
- isExpo,
480
- logoHeight,
481
- logoWidth,
482
- platforms
483
- });
484
- const iosOutputPath = getIOSOutputPath({
485
- assetsOutputPath,
486
- isExpo,
487
- platforms
488
- });
489
- const htmlTemplatePath = await getHtmlTemplatePath({
490
- isExpo,
491
- html,
492
- platforms
493
- });
494
- if (androidOutputPath != null) {
495
- log.title("🤖", "Android");
496
- hfs.ensureDir(androidOutputPath);
497
- await Promise.all([{
498
- ratio: 1,
499
- suffix: "mdpi"
500
- }, {
501
- ratio: 1.5,
502
- suffix: "hdpi"
503
- }, {
504
- ratio: 2,
505
- suffix: "xhdpi"
506
- }, {
507
- ratio: 3,
508
- suffix: "xxhdpi"
509
- }, {
510
- ratio: 4,
511
- suffix: "xxxhdpi"
512
- }].map(({
513
- ratio,
514
- suffix
515
- }) => {
516
- const drawableDirPath = path.resolve(androidOutputPath, `drawable-${suffix}`);
517
- hfs.ensureDir(drawableDirPath);
364
+ brand
365
+ } = props;
366
+ if (logo.width > 192 || logo.height > 192) {
367
+ return log.warn("Logo size exceeding 192x192dp will be cropped by Android. Skipping Android assets generation…");
368
+ }
369
+ if (brand != null && (brand.width > 200 || brand.height > 80)) {
370
+ return log.warn("Brand size exceeding 200x80dp will be cropped by Android. Skipping Android assets generation…");
371
+ }
372
+ if (logo.width > 134 || logo.height > 134) {
373
+ log.warn("Logo size exceeds 134x134dp. It might be cropped by Android.");
374
+ }
375
+ log.title("🤖", "Android");
376
+ hfs.ensureDir(androidOutputPath);
377
+ await Promise.all([{
378
+ ratio: 1,
379
+ suffix: "mdpi"
380
+ }, {
381
+ ratio: 1.5,
382
+ suffix: "hdpi"
383
+ }, {
384
+ ratio: 2,
385
+ suffix: "xhdpi"
386
+ }, {
387
+ ratio: 3,
388
+ suffix: "xxhdpi"
389
+ }, {
390
+ ratio: 4,
391
+ suffix: "xxxhdpi"
392
+ }].map(({
393
+ ratio,
394
+ suffix
395
+ }) => {
396
+ const drawableDirPath = path.resolve(androidOutputPath, `drawable-${suffix}`);
397
+ hfs.ensureDir(drawableDirPath);
518
398
 
519
- // https://developer.android.com/develop/ui/views/launch/splash-screen#dimensions
520
- const canvasSize = 288 * ratio;
399
+ // https://developer.android.com/develop/ui/views/launch/splash-screen#dimensions
400
+ const canvasSize = 288 * ratio;
521
401
 
522
- // https://sharp.pixelplumbing.com/api-constructor
523
- const canvas = sharp({
524
- create: {
525
- width: canvasSize,
526
- height: canvasSize,
527
- channels: 4,
528
- background: {
529
- r: 255,
530
- g: 255,
531
- b: 255,
532
- alpha: 0
533
- }
534
- }
535
- });
536
- const filePath = path.resolve(drawableDirPath, "bootsplash_logo.png");
537
- return logo.clone().resize(logoWidth * ratio).toBuffer().then(input => canvas.composite([{
538
- input
539
- }]).png({
540
- quality: 100
541
- }).toFile(filePath)).then(() => {
542
- log.write(filePath, {
543
- width: canvasSize,
544
- height: canvasSize
545
- });
546
- });
547
- }));
548
- if (!isExpo) {
549
- const manifestXmlPath = path.resolve(androidOutputPath, "..", "AndroidManifest.xml");
550
- if (hfs.exists(manifestXmlPath)) {
551
- const manifestXml = readXmlLike(manifestXmlPath);
552
- const activities = manifestXml.root.querySelectorAll("activity");
553
- for (const activity of activities) {
554
- if (activity.getAttribute("android:name") === ".MainActivity") {
555
- activity.setAttribute("android:theme", "@style/BootTheme");
556
- }
557
- }
558
- await writeXmlLike(manifestXmlPath, manifestXml.root.toString(), {
559
- ...manifestXml.formatOptions,
560
- formatter: "prettier",
561
- htmlWhitespaceSensitivity: "ignore",
562
- selfClosingTags: true,
563
- singleAttributePerLine: true
564
- });
565
- } else {
566
- log.warn("No AndroidManifest.xml found. Skipping…");
567
- }
568
- const valuesPath = path.resolve(androidOutputPath, "values");
569
- hfs.ensureDir(valuesPath);
570
- const colorsXmlPath = path.resolve(valuesPath, "colors.xml");
571
- const colorsXmlEntry = `<color name="bootsplash_background">${background.hex}</color>`;
572
- if (hfs.exists(colorsXmlPath)) {
573
- const colorsXml = readXmlLike(colorsXmlPath);
574
- const nextColor = parseHtml(colorsXmlEntry);
575
- const prevColor = colorsXml.root.querySelector('color[name="bootsplash_background"]');
576
- if (prevColor != null) {
577
- prevColor.replaceWith(nextColor);
578
- } else {
579
- colorsXml.root.querySelector("resources")?.appendChild(nextColor);
402
+ // https://sharp.pixelplumbing.com/api-constructor
403
+ const canvas = sharp({
404
+ create: {
405
+ width: canvasSize,
406
+ height: canvasSize,
407
+ channels: 4,
408
+ background: {
409
+ r: 255,
410
+ g: 255,
411
+ b: 255,
412
+ alpha: 0
580
413
  }
581
- await writeXmlLike(colorsXmlPath, colorsXml.root.toString(), {
582
- ...colorsXml.formatOptions,
583
- formatter: "xmlFormatter"
584
- });
585
- } else {
586
- await writeXmlLike(colorsXmlPath, `<resources>${colorsXmlEntry}</resources>`, {
587
- formatter: "xmlFormatter"
588
- });
589
- }
590
- const stylesXmlPath = path.resolve(valuesPath, "styles.xml");
591
- if (hfs.exists(stylesXmlPath)) {
592
- const stylesXml = readXmlLike(stylesXmlPath);
593
- const prevStyle = stylesXml.root.querySelector('style[name="BootTheme"]');
594
- const parent = prevStyle?.getAttribute("parent") ?? "Theme.BootSplash";
595
- const extraItems = parseHtml(prevStyle?.text.split("\n").map(line => line.trim()).join("") ?? "").childNodes.filter(node => {
596
- if (!(node instanceof HTMLElement)) {
597
- return true;
598
- }
599
- const name = node.getAttribute("name");
600
- return name !== "bootSplashBackground" && name !== "bootSplashLogo" && name !== "bootSplashBrand" && name !== "postBootSplashTheme";
601
- }).map(node => node.toString());
602
- const styleItems = [...(extraItems.length > 0 ? [...extraItems, ""] : []), '<item name="bootSplashBackground">@color/bootsplash_background</item>', '<item name="bootSplashLogo">@drawable/bootsplash_logo</item>', ...(brand != null && brandPath != null ? ['<item name="bootSplashBrand">@drawable/bootsplash_brand</item>'] : []), '<item name="postBootSplashTheme">@style/AppTheme</item>'];
603
- const nextStyle = parseHtml(dedent`
604
- <style name="BootTheme" parent="${parent}">
605
- ${styleItems.join("\n")}
606
- </style>
607
- `);
608
- prevStyle?.remove(); // remove the existing style
609
- stylesXml.root.querySelector("resources")?.appendChild(nextStyle);
610
- await writeXmlLike(stylesXmlPath, stylesXml.root.toString(), {
611
- ...stylesXml.formatOptions,
612
- formatter: "prettier",
613
- htmlWhitespaceSensitivity: "ignore"
614
- });
615
- } else {
616
- log.warn("No styles.xml found. Skipping…");
617
414
  }
618
- }
619
- }
620
- if (iosOutputPath != null) {
621
- log.title("🍏", "iOS");
622
- hfs.ensureDir(iosOutputPath);
623
- cleanIOSAssets(iosOutputPath);
624
- const storyboardPath = path.resolve(iosOutputPath, "BootSplash.storyboard");
625
- const colorsSetPath = path.resolve(iosOutputPath, "Colors.xcassets", `BootSplashBackground-${fileNameSuffix}.colorset`);
626
- const imageSetPath = path.resolve(iosOutputPath, "Images.xcassets", `BootSplashLogo-${fileNameSuffix}.imageset`);
627
- hfs.ensureDir(colorsSetPath);
628
- hfs.ensureDir(imageSetPath);
629
- await writeXmlLike(storyboardPath, getStoryboard({
630
- logoHeight,
631
- logoWidth,
632
- background: background.rgb,
633
- fileNameSuffix
634
- }), {
635
- formatter: "xmlFormatter",
636
- whiteSpaceAtEndOfSelfclosingTag: false
637
415
  });
638
- writeJson(path.resolve(colorsSetPath, "Contents.json"), {
639
- colors: [{
640
- idiom: "universal",
641
- color: {
642
- "color-space": "srgb",
643
- components: {
644
- blue: background.rgb.B,
645
- green: background.rgb.G,
646
- red: background.rgb.R,
647
- alpha: "1.000"
648
- }
649
- }
650
- }],
651
- info: {
652
- author: "xcode",
653
- version: 1
654
- }
416
+ const filePath = path.resolve(drawableDirPath, "bootsplash_logo.png");
417
+ return logo.image.clone().resize(logo.width * ratio).toBuffer().then(input => canvas.composite([{
418
+ input
419
+ }]).png({
420
+ quality: 100
421
+ }).toFile(filePath)).then(() => {
422
+ log.write(filePath, {
423
+ width: canvasSize,
424
+ height: canvasSize
425
+ });
655
426
  });
656
- const logoFileName = `logo-${fileNameSuffix}`;
657
- writeJson(path.resolve(imageSetPath, "Contents.json"), {
658
- images: [{
659
- idiom: "universal",
660
- filename: `${logoFileName}.png`,
661
- scale: "1x"
662
- }, {
663
- idiom: "universal",
664
- filename: `${logoFileName}@2x.png`,
665
- scale: "2x"
666
- }, {
667
- idiom: "universal",
668
- filename: `${logoFileName}@3x.png`,
669
- scale: "3x"
670
- }],
671
- info: {
672
- author: "xcode",
673
- version: 1
427
+ }));
428
+ };
429
+ const writeIOSAssets = async ({
430
+ iosOutputPath,
431
+ props
432
+ }) => {
433
+ const {
434
+ background,
435
+ logo,
436
+ fileNameSuffix
437
+ } = props;
438
+ log.title("🍏", "iOS");
439
+ hfs.ensureDir(iosOutputPath);
440
+
441
+ // clean existing assets
442
+ hfs.readDir(iosOutputPath).filter(file => file === "Colors.xcassets" || file === "Images.xcassets").map(file => path.join(iosOutputPath, file)).flatMap(dir => hfs.readDir(dir).filter(file => file.startsWith("BootSplash")).map(file => path.join(dir, file))).forEach(file => {
443
+ hfs.rm(file);
444
+ });
445
+ const storyboardPath = path.resolve(iosOutputPath, "BootSplash.storyboard");
446
+ await writeXmlLike(storyboardPath, getStoryboard(props), {
447
+ formatter: "xmlFormatter",
448
+ whiteSpaceAtEndOfSelfclosingTag: false
449
+ });
450
+ const colorsSetPath = path.resolve(iosOutputPath, "Colors.xcassets", `BootSplashBackground-${fileNameSuffix}.colorset`);
451
+ hfs.ensureDir(colorsSetPath);
452
+ writeJson(path.resolve(colorsSetPath, "Contents.json"), {
453
+ colors: [{
454
+ idiom: "universal",
455
+ color: {
456
+ "color-space": "srgb",
457
+ components: {
458
+ blue: background.rgb.B,
459
+ green: background.rgb.G,
460
+ red: background.rgb.R,
461
+ alpha: "1.000"
462
+ }
674
463
  }
675
- });
676
- await Promise.all([{
677
- ratio: 1,
678
- suffix: ""
464
+ }],
465
+ info: {
466
+ author: "xcode",
467
+ version: 1
468
+ }
469
+ });
470
+ const logoFileName = `logo-${fileNameSuffix}`;
471
+ const imagesSetPath = path.resolve(iosOutputPath, "Images.xcassets", `BootSplashLogo-${fileNameSuffix}.imageset`);
472
+ hfs.ensureDir(imagesSetPath);
473
+ writeJson(path.resolve(imagesSetPath, "Contents.json"), {
474
+ images: [{
475
+ idiom: "universal",
476
+ filename: `${logoFileName}.png`,
477
+ scale: "1x"
679
478
  }, {
680
- ratio: 2,
681
- suffix: "@2x"
479
+ idiom: "universal",
480
+ filename: `${logoFileName}@2x.png`,
481
+ scale: "2x"
682
482
  }, {
683
- ratio: 3,
684
- suffix: "@3x"
685
- }].map(({
686
- ratio,
687
- suffix
483
+ idiom: "universal",
484
+ filename: `${logoFileName}@3x.png`,
485
+ scale: "3x"
486
+ }],
487
+ info: {
488
+ author: "xcode",
489
+ version: 1
490
+ }
491
+ });
492
+ await Promise.all([{
493
+ ratio: 1,
494
+ suffix: ""
495
+ }, {
496
+ ratio: 2,
497
+ suffix: "@2x"
498
+ }, {
499
+ ratio: 3,
500
+ suffix: "@3x"
501
+ }].map(({
502
+ ratio,
503
+ suffix
504
+ }) => {
505
+ const filePath = path.resolve(imagesSetPath, `${logoFileName}${suffix}.png`);
506
+ return logo.image.clone().resize(logo.width * ratio).png({
507
+ quality: 100
508
+ }).toFile(filePath).then(({
509
+ width,
510
+ height
688
511
  }) => {
689
- const filePath = path.resolve(imageSetPath, `${logoFileName}${suffix}.png`);
690
- return logo.clone().resize(logoWidth * ratio).png({
691
- quality: 100
692
- }).toFile(filePath).then(({
512
+ log.write(filePath, {
693
513
  width,
694
514
  height
695
- }) => {
696
- log.write(filePath, {
697
- width,
698
- height
699
- });
700
515
  });
701
- }));
702
- if (!isExpo) {
703
- const infoPlistPath = path.resolve(iosOutputPath, "Info.plist");
704
- const infoPlist = plist.parse(hfs.text(infoPlistPath));
705
- infoPlist["UILaunchStoryboardName"] = "BootSplash";
706
- const formatted = formatXml(plist.build(infoPlist), {
707
- collapseContent: true,
708
- forceSelfClosingEmptyTag: false,
709
- indentation: "\t",
710
- lineSeparator: "\n",
711
- whiteSpaceAtEndOfSelfclosingTag: false
712
- }).replace(/<string\/>/gm, "<string></string>").replace(/^\t/gm, "");
713
- hfs.write(infoPlistPath, formatted);
714
- log.write(infoPlistPath);
715
- const pbxprojectPath = Expo.IOSConfig.Paths.getPBXProjectPath(projectRoot);
716
- const xcodeProjectPath = Expo.IOSConfig.Paths.getXcodeProjectPath(projectRoot);
717
- const project = Expo.IOSConfig.XcodeUtils.getPbxproj(projectRoot);
718
- const projectName = path.basename(iosOutputPath);
719
- const groupName = path.parse(xcodeProjectPath).name;
720
- Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
721
- project,
722
- filepath: path.join(projectName, "BootSplash.storyboard"),
723
- groupName,
724
- isBuildFile: true
725
- });
726
- Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
727
- project,
728
- filepath: path.join(projectName, "Colors.xcassets"),
729
- groupName,
730
- isBuildFile: true
731
- });
732
- hfs.write(pbxprojectPath, project.writeSync());
733
- log.write(pbxprojectPath);
734
- }
735
- }
736
- if (htmlTemplatePath != null) {
737
- log.title("🌐", "Web");
738
- const htmlTemplate = readXmlLike(htmlTemplatePath);
739
- const {
740
- format
741
- } = await logo.metadata();
742
- const prevStyle = htmlTemplate.root.querySelector("#bootsplash-style");
743
- const base64 = (format === "svg" ? hfs.buffer(logoPath) : await logo.clone().resize(Math.round(logoWidth * 2)).png({
744
- quality: 100
745
- }).toBuffer()).toString("base64");
746
- const dataURI = `data:image/${format ? "svg+xml" : "png"};base64,${base64}`;
747
- const nextStyle = parseHtml(dedent`
748
- <style id="bootsplash-style">
749
- #bootsplash {
750
- position: absolute;
751
- top: 0;
752
- bottom: 0;
753
- left: 0;
754
- right: 0;
755
- overflow: hidden;
756
- display: flex;
757
- justify-content: center;
758
- align-items: center;
759
- background-color: ${background.hex};
760
- }
761
- #bootsplash-logo {
762
- content: url("${dataURI}");
763
- width: ${logoWidth}px;
764
- height: ${logoHeight}px;
765
- }
766
- </style>
767
- `);
768
- if (prevStyle != null) {
769
- prevStyle.replaceWith(nextStyle);
770
- } else {
771
- htmlTemplate.root.querySelector("head")?.appendChild(nextStyle);
772
- }
773
- const prevDiv = htmlTemplate.root.querySelector("#bootsplash");
774
- const nextDiv = parseHtml(dedent`
775
- <div id="bootsplash">
776
- <div id="bootsplash-logo"></div>
777
- </div>
778
- `);
779
- if (prevDiv != null) {
780
- prevDiv.replaceWith(nextDiv);
781
- } else {
782
- htmlTemplate.root.querySelector("body")?.appendChild(nextDiv);
783
- }
784
- await writeXmlLike(htmlTemplatePath, htmlTemplate.root.toString(), {
785
- ...htmlTemplate.formatOptions,
786
- formatter: "prettier",
787
- useCssPlugin: true
788
516
  });
517
+ }));
518
+ };
519
+ const writeWebAssets = async ({
520
+ htmlTemplatePath,
521
+ props
522
+ }) => {
523
+ const {
524
+ background,
525
+ logo
526
+ } = props;
527
+ log.title("🌐", "Web");
528
+ const htmlTemplate = readXmlLike(htmlTemplatePath);
529
+ const {
530
+ format
531
+ } = await logo.image.metadata();
532
+ const prevStyle = htmlTemplate.root.querySelector("#bootsplash-style");
533
+ const base64 = (format === "svg" ? hfs.buffer(logo.path) : await logo.image.clone().resize(Math.round(logo.width * 2)).png({
534
+ quality: 100
535
+ }).toBuffer()).toString("base64");
536
+ const dataURI = `data:image/${format ? "svg+xml" : "png"};base64,${base64}`;
537
+ const nextStyle = parseHtml(dedent`
538
+ <style id="bootsplash-style">
539
+ #bootsplash {
540
+ position: absolute;
541
+ top: 0;
542
+ bottom: 0;
543
+ left: 0;
544
+ right: 0;
545
+ overflow: hidden;
546
+ display: flex;
547
+ justify-content: center;
548
+ align-items: center;
549
+ background-color: ${background.hex};
550
+ }
551
+ #bootsplash-logo {
552
+ content: url("${dataURI}");
553
+ width: ${logo.width}px;
554
+ height: ${logo.height}px;
555
+ }
556
+ </style>
557
+ `);
558
+ if (prevStyle != null) {
559
+ prevStyle.replaceWith(nextStyle);
560
+ } else {
561
+ htmlTemplate.root.querySelector("head")?.appendChild(nextStyle);
562
+ }
563
+ const prevDiv = htmlTemplate.root.querySelector("#bootsplash");
564
+ const nextDiv = parseHtml(dedent`
565
+ <div id="bootsplash">
566
+ <div id="bootsplash-logo"></div>
567
+ </div>
568
+ `);
569
+ if (prevDiv != null) {
570
+ prevDiv.replaceWith(nextDiv);
571
+ } else {
572
+ htmlTemplate.root.querySelector("body")?.appendChild(nextDiv);
789
573
  }
574
+ await writeXmlLike(htmlTemplatePath, htmlTemplate.root.toString(), {
575
+ ...htmlTemplate.formatOptions,
576
+ formatter: "prettier",
577
+ useCssPlugin: true
578
+ });
579
+ };
580
+ const writeGenericAssets = async ({
581
+ props
582
+ }) => {
583
+ const {
584
+ assetsOutputPath,
585
+ background,
586
+ logo
587
+ } = props;
790
588
  log.title("📄", "Assets");
791
589
  hfs.ensureDir(assetsOutputPath);
792
590
  writeJson(path.resolve(assetsOutputPath, "manifest.json"), {
793
591
  background: background.hex,
794
592
  logo: {
795
- width: logoWidth,
796
- height: logoHeight
593
+ width: logo.width,
594
+ height: logo.height
797
595
  }
798
596
  });
799
597
  await Promise.all([{
@@ -816,7 +614,7 @@ export const generate = async ({
816
614
  suffix
817
615
  }) => {
818
616
  const filePath = path.resolve(assetsOutputPath, `logo${suffix}.png`);
819
- return logo.clone().resize(Math.round(logoWidth * ratio)).png({
617
+ return logo.image.clone().resize(Math.round(logo.width * ratio)).png({
820
618
  quality: 100
821
619
  }).toFile(filePath).then(({
822
620
  width,
@@ -828,30 +626,175 @@ export const generate = async ({
828
626
  });
829
627
  });
830
628
  }));
629
+ };
630
+ const requireAddon = ({
631
+ executeAddon,
632
+ licenseKey
633
+ }) => {
831
634
  if (licenseKey != null && executeAddon) {
832
- const addon = requireAddon();
833
- await addon?.execute({
834
- licenseKey,
835
- isExpo,
836
- fileNameSuffix,
635
+ try {
636
+ return require("./addon");
637
+ } catch {
638
+ return;
639
+ }
640
+ }
641
+ };
642
+ export const generate = async ({
643
+ platforms,
644
+ html,
645
+ flavor,
646
+ plist,
647
+ ...rawProps
648
+ }) => {
649
+ const props = await transformProps(workingPath, rawProps);
650
+ const addon = requireAddon(props);
651
+ const {
652
+ background,
653
+ brand
654
+ } = props;
655
+ const androidOutputPath = getAndroidOutputPath({
656
+ flavor,
657
+ platforms
658
+ });
659
+ const iosOutputPath = getIOSOutputPath({
660
+ platforms
661
+ });
662
+ const htmlTemplatePath = getHtmlTemplatePath({
663
+ html,
664
+ platforms
665
+ });
666
+ if (androidOutputPath != null) {
667
+ await writeAndroidAssets({
837
668
  androidOutputPath,
669
+ props
670
+ });
671
+ const manifestXmlPath = path.resolve(androidOutputPath, "..", "AndroidManifest.xml");
672
+ if (hfs.exists(manifestXmlPath)) {
673
+ const manifestXml = readXmlLike(manifestXmlPath);
674
+ const activities = manifestXml.root.querySelectorAll("activity");
675
+ for (const activity of activities) {
676
+ if (activity.getAttribute("android:name") === ".MainActivity") {
677
+ activity.setAttribute("android:theme", "@style/BootTheme");
678
+ }
679
+ }
680
+ await writeXmlLike(manifestXmlPath, manifestXml.root.toString(), {
681
+ ...manifestXml.formatOptions,
682
+ formatter: "prettier",
683
+ htmlWhitespaceSensitivity: "ignore",
684
+ selfClosingTags: true,
685
+ singleAttributePerLine: true
686
+ });
687
+ } else {
688
+ log.warn("No AndroidManifest.xml found");
689
+ }
690
+ const valuesPath = path.resolve(androidOutputPath, "values");
691
+ hfs.ensureDir(valuesPath);
692
+ const colorsXmlPath = path.resolve(valuesPath, "colors.xml");
693
+ const colorsXmlEntry = `<color name="bootsplash_background">${background.hex}</color>`;
694
+ if (hfs.exists(colorsXmlPath)) {
695
+ const colorsXml = readXmlLike(colorsXmlPath);
696
+ const nextColor = parseHtml(colorsXmlEntry);
697
+ const prevColor = colorsXml.root.querySelector('color[name="bootsplash_background"]');
698
+ if (prevColor != null) {
699
+ prevColor.replaceWith(nextColor);
700
+ } else {
701
+ colorsXml.root.querySelector("resources")?.appendChild(nextColor);
702
+ }
703
+ await writeXmlLike(colorsXmlPath, colorsXml.root.toString(), {
704
+ ...colorsXml.formatOptions,
705
+ formatter: "xmlFormatter"
706
+ });
707
+ } else {
708
+ await writeXmlLike(colorsXmlPath, `<resources>${colorsXmlEntry}</resources>`, {
709
+ formatter: "xmlFormatter"
710
+ });
711
+ }
712
+ const stylesXmlPath = path.resolve(valuesPath, "styles.xml");
713
+ if (hfs.exists(stylesXmlPath)) {
714
+ const stylesXml = readXmlLike(stylesXmlPath);
715
+ const prevStyle = stylesXml.root.querySelector('style[name="BootTheme"]');
716
+ const parent = prevStyle?.getAttribute("parent") ?? "Theme.BootSplash";
717
+ const extraItems = parseHtml(prevStyle?.text.split("\n").map(line => line.trim()).join("") ?? "").childNodes.filter(node => {
718
+ if (!(node instanceof HTMLElement)) {
719
+ return true;
720
+ }
721
+ const name = node.getAttribute("name");
722
+ return name !== "bootSplashBackground" && name !== "bootSplashLogo" && name !== "bootSplashBrand" && name !== "postBootSplashTheme";
723
+ }).map(node => node.toString());
724
+ const styleItems = [...(extraItems.length > 0 ? [...extraItems, ""] : []), '<item name="bootSplashBackground">@color/bootsplash_background</item>', '<item name="bootSplashLogo">@drawable/bootsplash_logo</item>', ...(brand != null ? ['<item name="bootSplashBrand">@drawable/bootsplash_brand</item>'] : []), '<item name="postBootSplashTheme">@style/AppTheme</item>'];
725
+ const nextStyle = parseHtml(dedent`
726
+ <style name="BootTheme" parent="${parent}">
727
+ ${styleItems.join("\n")}
728
+ </style>
729
+ `);
730
+ prevStyle?.remove(); // remove the existing style
731
+ stylesXml.root.querySelector("resources")?.appendChild(nextStyle);
732
+ await writeXmlLike(stylesXmlPath, stylesXml.root.toString(), {
733
+ ...stylesXml.formatOptions,
734
+ formatter: "prettier",
735
+ htmlWhitespaceSensitivity: "ignore"
736
+ });
737
+ } else {
738
+ log.warn("No styles.xml found");
739
+ }
740
+ }
741
+ if (iosOutputPath != null) {
742
+ await writeIOSAssets({
838
743
  iosOutputPath,
744
+ props
745
+ });
746
+ const infoPlistPath = getInfoPlistPath({
747
+ iosOutputPath,
748
+ plist
749
+ });
750
+ if (infoPlistPath != null) {
751
+ const infoPlist = ExpoPlist.parse(hfs.text(infoPlistPath));
752
+ infoPlist["UILaunchStoryboardName"] = "BootSplash";
753
+ const formatted = formatXml(ExpoPlist.build(infoPlist), {
754
+ collapseContent: true,
755
+ forceSelfClosingEmptyTag: false,
756
+ indentation: "\t",
757
+ lineSeparator: "\n",
758
+ whiteSpaceAtEndOfSelfclosingTag: false
759
+ }).replace(/<string\/>/gm, "<string></string>").replace(/^\t/gm, "");
760
+ hfs.write(infoPlistPath, formatted);
761
+ log.write(infoPlistPath);
762
+ }
763
+ const pbxprojectPath = Expo.IOSConfig.Paths.getPBXProjectPath(projectRoot);
764
+ const xcodeProjectPath = Expo.IOSConfig.Paths.getXcodeProjectPath(projectRoot);
765
+ const project = Expo.IOSConfig.XcodeUtils.getPbxproj(projectRoot);
766
+ const projectName = path.basename(iosOutputPath);
767
+ const groupName = path.parse(xcodeProjectPath).name;
768
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
769
+ project,
770
+ filepath: path.join(projectName, "BootSplash.storyboard"),
771
+ groupName,
772
+ isBuildFile: true
773
+ });
774
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
775
+ project,
776
+ filepath: path.join(projectName, "Colors.xcassets"),
777
+ groupName,
778
+ isBuildFile: true
779
+ });
780
+ hfs.write(pbxprojectPath, project.writeSync());
781
+ log.write(pbxprojectPath);
782
+ }
783
+ if (htmlTemplatePath != null) {
784
+ await writeWebAssets({
839
785
  htmlTemplatePath,
840
- assetsOutputPath,
841
- logoHeight,
842
- logoWidth,
843
- brandHeight,
844
- brandWidth,
845
- logoPath,
846
- darkLogoPath,
847
- brandPath,
848
- darkBrandPath,
849
- background,
850
- logo,
851
- brand,
852
- darkBackground,
853
- darkLogo,
854
- darkBrand
786
+ props
787
+ });
788
+ }
789
+ await writeGenericAssets({
790
+ props
791
+ });
792
+ if (addon != null) {
793
+ await addon.execute({
794
+ props,
795
+ androidOutputPath,
796
+ iosOutputPath,
797
+ htmlTemplatePath
855
798
  });
856
799
  } else {
857
800
  console.log(`
@@ -862,4 +805,308 @@ ${pc.blue("┗━━━━━━━━━━━━━━━━━━━━━━
862
805
  }
863
806
  console.log(`\n💖 Thanks for using ${pc.underline("react-native-bootsplash")}`);
864
807
  };
808
+
809
+ // Expo plugin
810
+
811
+ const withoutExpoSplashScreen = Expo.createRunOncePlugin(expoConfig => expoConfig, "expo-splash-screen", "skip");
812
+ const withAndroidAssets = (expoConfig, rawProps) => Expo.withDangerousMod(expoConfig, ["android", async config => {
813
+ const {
814
+ platformProjectRoot,
815
+ projectRoot
816
+ } = config.modRequest;
817
+ const props = await transformProps(projectRoot, rawProps);
818
+ const addon = requireAddon(props);
819
+ const androidOutputPath = path.resolve(platformProjectRoot, "app", "src", "main", "res");
820
+ await writeAndroidAssets({
821
+ androidOutputPath,
822
+ props
823
+ });
824
+ await addon?.writeAndroidAssets({
825
+ androidOutputPath,
826
+ props
827
+ });
828
+ return config;
829
+ }]);
830
+ const withAndroidManifest = expoConfig => Expo.withAndroidManifest(expoConfig, config => {
831
+ config.modResults.manifest.application?.forEach(application => {
832
+ if (application.$["android:name"] === ".MainApplication") {
833
+ const {
834
+ activity
835
+ } = application;
836
+ activity?.forEach(activity => {
837
+ if (activity.$["android:name"] === ".MainActivity") {
838
+ activity.$["android:theme"] = "@style/BootTheme";
839
+ }
840
+ });
841
+ }
842
+ });
843
+ return config;
844
+ });
845
+ const withMainActivity = expoConfig => Expo.withMainActivity(expoConfig, config => {
846
+ const {
847
+ modResults
848
+ } = config;
849
+ const withImports = addImports(modResults.contents.replace(/(\/\/ )?setTheme\(R\.style\.AppTheme\)/, "// setTheme(R.style.AppTheme)"), ["android.os.Bundle", "com.zoontek.rnbootsplash.RNBootSplash"], false);
850
+
851
+ // indented with 4 spaces
852
+ const withInit = mergeContents({
853
+ src: withImports,
854
+ comment: " //",
855
+ tag: "bootsplash-init",
856
+ offset: 0,
857
+ anchor: /super\.onCreate\(null\)/,
858
+ newSrc: " RNBootSplash.init(this, R.style.BootTheme)"
859
+ });
860
+ return {
861
+ ...config,
862
+ modResults: {
863
+ ...modResults,
864
+ contents: withInit.contents
865
+ }
866
+ };
867
+ });
868
+ const withAndroidStyles = (expoConfig, rawProps) => Expo.withAndroidStyles(expoConfig, async config => {
869
+ const {
870
+ projectRoot
871
+ } = config.modRequest;
872
+ const {
873
+ android,
874
+ brand
875
+ } = await transformProps(projectRoot, rawProps);
876
+ const {
877
+ darkContentBarsStyle
878
+ } = android;
879
+ const {
880
+ modResults
881
+ } = config;
882
+ const {
883
+ resources
884
+ } = modResults;
885
+ const {
886
+ style = []
887
+ } = resources;
888
+ const item = [{
889
+ $: {
890
+ name: "postBootSplashTheme"
891
+ },
892
+ _: "@style/AppTheme"
893
+ }, {
894
+ $: {
895
+ name: "bootSplashBackground"
896
+ },
897
+ _: "@color/bootsplash_background"
898
+ }, {
899
+ $: {
900
+ name: "bootSplashLogo"
901
+ },
902
+ _: "@drawable/bootsplash_logo"
903
+ }];
904
+ if (brand != null) {
905
+ item.push({
906
+ $: {
907
+ name: "bootSplashBrand"
908
+ },
909
+ _: "@drawable/bootsplash_brand"
910
+ });
911
+ }
912
+ if (darkContentBarsStyle != null) {
913
+ item.push({
914
+ $: {
915
+ name: "darkContentBarsStyle"
916
+ },
917
+ _: String(darkContentBarsStyle)
918
+ });
919
+ }
920
+ const withBootTheme = [...style.filter(({
921
+ $
922
+ }) => $.name !== "BootTheme"), {
923
+ $: {
924
+ name: "BootTheme",
925
+ parent: "Theme.BootSplash"
926
+ },
927
+ item
928
+ }];
929
+ return {
930
+ ...config,
931
+ modResults: {
932
+ ...modResults,
933
+ resources: {
934
+ ...resources,
935
+ style: withBootTheme
936
+ }
937
+ }
938
+ };
939
+ });
940
+ const withAndroidColors = (expoConfig, rawProps) => Expo.withAndroidColors(expoConfig, async config => {
941
+ const {
942
+ projectRoot
943
+ } = config.modRequest;
944
+ const {
945
+ background
946
+ } = await transformProps(projectRoot, rawProps);
947
+ config.modResults = assignColorValue(config.modResults, {
948
+ name: "bootsplash_background",
949
+ value: background.hex
950
+ });
951
+ return config;
952
+ });
953
+ const withAndroidColorsNight = (expoConfig, rawProps) => Expo.withAndroidColorsNight(expoConfig, async config => {
954
+ const {
955
+ projectRoot
956
+ } = config.modRequest;
957
+ const props = await transformProps(projectRoot, rawProps);
958
+ const addon = requireAddon(props);
959
+ return addon != null ? await addon.withAndroidColorsNight({
960
+ config,
961
+ props
962
+ }) : config;
963
+ });
964
+ const withIOSAssets = (expoConfig, rawProps) => Expo.withDangerousMod(expoConfig, ["ios", async config => {
965
+ const {
966
+ platformProjectRoot,
967
+ projectName = "",
968
+ projectRoot
969
+ } = config.modRequest;
970
+ const props = await transformProps(projectRoot, rawProps);
971
+ const addon = requireAddon(props);
972
+ const iosOutputPath = path.resolve(platformProjectRoot, projectName);
973
+ await writeIOSAssets({
974
+ iosOutputPath,
975
+ props
976
+ });
977
+ await addon?.writeIOSAssets({
978
+ iosOutputPath,
979
+ props
980
+ });
981
+ return config;
982
+ }]);
983
+ const withAppDelegate = expoConfig => Expo.withAppDelegate(expoConfig, config => {
984
+ const {
985
+ modResults
986
+ } = config;
987
+ const {
988
+ language
989
+ } = modResults;
990
+ if (language !== "swift") {
991
+ throw new Error(`Cannot modify the project AppDelegate as it's not in a supported language: ${language}`);
992
+ }
993
+ const withHeader = mergeContents({
994
+ src: modResults.contents,
995
+ comment: "//",
996
+ tag: "bootsplash-header",
997
+ offset: 1,
998
+ anchor: /import Expo/,
999
+ newSrc: "import RNBootSplash"
1000
+ });
1001
+ const withRootView = mergeContents({
1002
+ src: withHeader.contents,
1003
+ comment: "//",
1004
+ tag: "bootsplash-init",
1005
+ offset: 1,
1006
+ anchor: /class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {/,
1007
+ newSrc: dedent`
1008
+ public override func customize(_ rootView: UIView) {
1009
+ super.customize(rootView)
1010
+ RNBootSplash.initWithStoryboard("BootSplash", rootView: rootView)
1011
+ }
1012
+ `
1013
+ });
1014
+ return {
1015
+ ...config,
1016
+ modResults: {
1017
+ ...modResults,
1018
+ contents: withRootView.contents
1019
+ }
1020
+ };
1021
+ });
1022
+ const withInfoPlist = expoConfig => Expo.withInfoPlist(expoConfig, config => {
1023
+ config.modResults["UILaunchStoryboardName"] = "BootSplash";
1024
+ return config;
1025
+ });
1026
+ const withXcodeProject = expoConfig => Expo.withXcodeProject(expoConfig, config => {
1027
+ const {
1028
+ projectName = ""
1029
+ } = config.modRequest;
1030
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
1031
+ filepath: path.join(projectName, "BootSplash.storyboard"),
1032
+ groupName: projectName,
1033
+ project: config.modResults,
1034
+ isBuildFile: true
1035
+ });
1036
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
1037
+ filepath: path.join(projectName, "Colors.xcassets"),
1038
+ groupName: projectName,
1039
+ project: config.modResults,
1040
+ isBuildFile: true
1041
+ });
1042
+ return config;
1043
+ });
1044
+ const withWebAssets = (expoConfig, rawProps) => Expo.withDangerousMod(expoConfig, [expoConfig.platforms?.includes("ios") ? "ios" : "android", async config => {
1045
+ const {
1046
+ projectRoot
1047
+ } = config.modRequest;
1048
+ const props = await transformProps(projectRoot, rawProps);
1049
+ const addon = requireAddon(props);
1050
+ const fileName = "public/index.html";
1051
+ const htmlTemplatePath = path.resolve(projectRoot, fileName);
1052
+ if (!hfs.exists(htmlTemplatePath)) {
1053
+ await exec(`npx expo customize ${fileName}`);
1054
+ }
1055
+ await writeWebAssets({
1056
+ htmlTemplatePath,
1057
+ props
1058
+ });
1059
+ await addon?.writeWebAssets({
1060
+ htmlTemplatePath,
1061
+ props
1062
+ });
1063
+ return config;
1064
+ }]);
1065
+ const withGenericAssets = (expoConfig, rawProps) => Expo.withDangerousMod(expoConfig, [expoConfig.platforms?.includes("ios") ? "ios" : "android", async config => {
1066
+ const {
1067
+ projectRoot
1068
+ } = config.modRequest;
1069
+ const props = await transformProps(projectRoot, rawProps);
1070
+ const addon = requireAddon(props);
1071
+ await writeGenericAssets({
1072
+ props
1073
+ });
1074
+ await addon?.writeGenericAssets({
1075
+ props
1076
+ });
1077
+ return config;
1078
+ }]);
1079
+ export const withBootSplash = Expo.createRunOncePlugin((config, baseProps) => {
1080
+ const {
1081
+ platforms = [],
1082
+ sdkVersion = "0.1.0"
1083
+ } = config;
1084
+ isExpo = true;
1085
+ if (semver.lt(sdkVersion, "54.0.0")) {
1086
+ log.error("Requires Expo 54.0.0 (or higher)");
1087
+ process.exit(1);
1088
+ }
1089
+ if (!baseProps?.logo) {
1090
+ log.error("Missing required parameter 'logo'");
1091
+ process.exit(1);
1092
+ }
1093
+ const rawProps = {
1094
+ assetsOutput: "assets/bootsplash",
1095
+ background: "#fff",
1096
+ brandWidth: 80,
1097
+ logoWidth: 100,
1098
+ ...baseProps
1099
+ };
1100
+ const plugins = [withoutExpoSplashScreen, withGenericAssets];
1101
+ if (platforms.includes("android")) {
1102
+ plugins.push(withAndroidAssets, withAndroidManifest, withMainActivity, withAndroidStyles, withAndroidColors, withAndroidColorsNight);
1103
+ }
1104
+ if (platforms.includes("ios")) {
1105
+ plugins.push(withIOSAssets, withAppDelegate, withInfoPlist, withXcodeProject);
1106
+ }
1107
+ if (platforms.includes("web")) {
1108
+ plugins.push(withWebAssets);
1109
+ }
1110
+ return Expo.withPlugins(config, plugins.map(plugin => [plugin, rawProps]));
1111
+ }, PACKAGE_NAME);
865
1112
  //# sourceMappingURL=generate.js.map