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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,13 @@
1
1
  import murmurhash from "@emotion/hash";
2
- import { IOSConfig } from "@expo/config-plugins";
2
+ import { getConfig as getExpoConfig } from "@expo/config";
3
+ import * as Expo from "@expo/config-plugins";
4
+ import { assignColorValue } from "@expo/config-plugins/build/android/Colors";
5
+ import { addImports } from "@expo/config-plugins/build/android/codeMod";
6
+ import { mergeContents } from "@expo/config-plugins/build/utils/generateCode";
3
7
  import plist from "@expo/plist";
4
8
  import { findProjectRoot } from "@react-native-community/cli-tools";
5
9
  import detectIndent from "detect-indent";
6
- import fs from "fs";
10
+ import fs from "fs-extra";
7
11
  import { parse as parseHtml } from "node-html-parser";
8
12
  import path from "path";
9
13
  import pc from "picocolors";
@@ -14,6 +18,21 @@ import sharp from "sharp";
14
18
  import { dedent } from "ts-dedent";
15
19
  import formatXml from "xml-formatter";
16
20
  const workingPath = process.env.INIT_CWD ?? process.env.PWD ?? process.cwd();
21
+ const projectRoot = findProjectRoot(workingPath);
22
+ export const log = {
23
+ error: text => {
24
+ console.log(pc.red(`❌ ${text}`));
25
+ },
26
+ title: (emoji, text) => {
27
+ console.log(`\n${emoji} ${pc.underline(pc.bold(text))}`);
28
+ },
29
+ warn: text => {
30
+ console.log(pc.yellow(`⚠️ ${text}`));
31
+ },
32
+ write: (filePath, dimensions) => {
33
+ console.log(` ${path.relative(workingPath, filePath)}` + (dimensions != null ? ` (${dimensions.width}x${dimensions.height})` : ""));
34
+ }
35
+ };
17
36
  export const parseColor = value => {
18
37
  const up = value.toUpperCase().replace(/[^0-9A-F]/g, "");
19
38
  if (up.length !== 3 && up.length !== 6) {
@@ -22,12 +41,12 @@ export const parseColor = value => {
22
41
  }
23
42
  const hex = up.length === 3 ? "#" + up[0] + up[0] + up[1] + up[1] + up[2] + up[2] : "#" + up;
24
43
  const rgb = {
25
- R: (parseInt("" + hex[1] + hex[2], 16) / 255).toPrecision(15),
26
- G: (parseInt("" + hex[3] + hex[4], 16) / 255).toPrecision(15),
27
- B: (parseInt("" + hex[5] + hex[6], 16) / 255).toPrecision(15)
44
+ R: (Number.parseInt("" + hex[1] + hex[2], 16) / 255).toPrecision(15),
45
+ G: (Number.parseInt("" + hex[3] + hex[4], 16) / 255).toPrecision(15),
46
+ B: (Number.parseInt("" + hex[5] + hex[6], 16) / 255).toPrecision(15)
28
47
  };
29
48
  return {
30
- hex,
49
+ hex: hex.toLowerCase(),
31
50
  rgb
32
51
  };
33
52
  };
@@ -38,7 +57,8 @@ const getStoryboard = ({
38
57
  R,
39
58
  G,
40
59
  B
41
- }
60
+ },
61
+ fileNameSuffix
42
62
  }) => {
43
63
  const frameWidth = 375;
44
64
  const frameHeight = 667;
@@ -51,6 +71,7 @@ const getStoryboard = ({
51
71
  <dependencies>
52
72
  <deployment identifier="iOS"/>
53
73
  <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
74
+ <capability name="Named colors" minToolsVersion="9.0"/>
54
75
  <capability name="Safe area layout guides" minToolsVersion="9.0"/>
55
76
  <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
56
77
  </dependencies>
@@ -63,7 +84,7 @@ const getStoryboard = ({
63
84
  <rect key="frame" x="0.0" y="0.0" width="${frameWidth}" height="${frameHeight}"/>
64
85
  <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
65
86
  <subviews>
66
- <imageView autoresizesSubviews="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="BootSplashLogo" translatesAutoresizingMaskIntoConstraints="NO" id="3lX-Ut-9ad">
87
+ <imageView autoresizesSubviews="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="BootSplashLogo-${fileNameSuffix}" translatesAutoresizingMaskIntoConstraints="NO" id="3lX-Ut-9ad">
67
88
  <rect key="frame" x="${logoX}" y="${logoY}" width="${logoWidth}" height="${logoHeight}"/>
68
89
  <accessibility key="accessibilityConfiguration">
69
90
  <accessibilityTraits key="traits" image="YES" notEnabled="YES"/>
@@ -71,7 +92,7 @@ const getStoryboard = ({
71
92
  </imageView>
72
93
  </subviews>
73
94
  <viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
74
- <color key="backgroundColor" red="${R}" green="${G}" blue="${B}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
95
+ <color key="backgroundColor" name="BootSplashBackground-${fileNameSuffix}"/>
75
96
  <constraints>
76
97
  <constraint firstItem="3lX-Ut-9ad" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="Fh9-Fy-1nT"/>
77
98
  <constraint firstItem="3lX-Ut-9ad" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="nvB-Ic-PnI"/>
@@ -84,60 +105,58 @@ const getStoryboard = ({
84
105
  </scene>
85
106
  </scenes>
86
107
  <resources>
87
- <image name="BootSplashLogo" width="${logoWidth}" height="${logoHeight}"/>
108
+ <image name="BootSplashLogo-${fileNameSuffix}" width="${logoWidth}" height="${logoHeight}"/>
109
+ <namedColor name="BootSplashBackground-${fileNameSuffix}">
110
+ <color red="${R}" green="${G}" blue="${B}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
111
+ </namedColor>
88
112
  </resources>
89
113
  </document>
90
114
  `;
91
115
  };
92
- export const addFileToXcodeProject = filePath => {
93
- const projectRoot = findProjectRoot(workingPath);
94
- const pbxprojectPath = IOSConfig.Paths.getPBXProjectPath(projectRoot);
95
- const project = IOSConfig.XcodeUtils.getPbxproj(projectRoot);
96
- const xcodeProjectPath = IOSConfig.Paths.getXcodeProjectPath(projectRoot);
97
- IOSConfig.XcodeUtils.addResourceFileToGroup({
98
- filepath: filePath,
99
- groupName: path.parse(xcodeProjectPath).name,
100
- project,
101
- isBuildFile: true
102
- });
103
- hfs.write(pbxprojectPath, project.writeSync());
104
- logWrite(pbxprojectPath);
105
- };
106
116
 
107
117
  // Freely inspired by https://github.com/humanwhocodes/humanfs
108
118
  export const hfs = {
109
119
  buffer: path => fs.readFileSync(path),
110
120
  exists: path => fs.existsSync(path),
121
+ isDir: path => fs.lstatSync(path).isDirectory(),
111
122
  json: path => JSON.parse(fs.readFileSync(path, "utf-8")),
112
123
  readDir: path => fs.readdirSync(path, "utf-8"),
113
124
  realPath: path => fs.realpathSync(path, "utf-8"),
114
125
  rm: path => fs.rmSync(path, {
115
- force: true
126
+ force: true,
127
+ recursive: true
116
128
  }),
117
129
  text: path => fs.readFileSync(path, "utf-8"),
130
+ copy: (src, dest) => {
131
+ if (hfs.isDir(src) || !hfs.exists(dest)) {
132
+ return fs.copySync(src, dest, {
133
+ overwrite: true
134
+ });
135
+ }
136
+ const srcBuffer = fs.readFileSync(src);
137
+ const destBuffer = fs.readFileSync(dest);
138
+ if (!srcBuffer.equals(destBuffer)) {
139
+ return fs.copySync(src, dest, {
140
+ overwrite: true
141
+ });
142
+ }
143
+ },
118
144
  ensureDir: dir => {
119
145
  fs.mkdirSync(dir, {
120
146
  recursive: true
121
147
  });
122
148
  },
123
- write: (file, data) => {
124
- const trimmed = data.trim();
125
- fs.writeFileSync(file, trimmed === "" ? trimmed : trimmed + "\n", "utf-8");
149
+ write: (path, content) => {
150
+ const trimmed = content.trim();
151
+ fs.writeFileSync(path, trimmed === "" ? trimmed : trimmed + "\n", "utf-8");
126
152
  }
127
153
  };
128
- export const log = {
129
- error: text => console.log(pc.red(`❌ ${text}`)),
130
- text: text => console.log(text),
131
- title: (emoji, text) => console.log(`\n${emoji} ${pc.underline(pc.bold(text))}`),
132
- warn: text => console.log(pc.yellow(`⚠️ ${text}`))
154
+ export const writeJson = (filePath, content) => {
155
+ hfs.write(filePath, JSON.stringify(content, null, 2));
156
+ log.write(filePath);
133
157
  };
134
- export const logWrite = (filePath, dimensions) => console.log(` ${path.relative(workingPath, filePath)}` + (dimensions != null ? ` (${dimensions.width}x${dimensions.height})` : ""));
135
- export const writeJson = (file, json) => {
136
- hfs.write(file, JSON.stringify(json, null, 2));
137
- logWrite(file);
138
- };
139
- export const readXml = file => {
140
- const xml = hfs.text(file);
158
+ export const readXml = filePath => {
159
+ const xml = hfs.text(filePath);
141
160
  const {
142
161
  indent
143
162
  } = detectIndent(xml);
@@ -149,8 +168,8 @@ export const readXml = file => {
149
168
  formatOptions
150
169
  };
151
170
  };
152
- export const writeXml = (file, xml, options) => {
153
- const formatted = formatXml(xml, {
171
+ export const writeXml = (filePath, content, options) => {
172
+ const formatted = formatXml(content, {
154
173
  collapseContent: true,
155
174
  forceSelfClosingEmptyTag: true,
156
175
  indentation: " ",
@@ -158,11 +177,11 @@ export const writeXml = (file, xml, options) => {
158
177
  whiteSpaceAtEndOfSelfclosingTag: true,
159
178
  ...options
160
179
  });
161
- hfs.write(file, formatted);
162
- logWrite(file);
180
+ hfs.write(filePath, formatted);
181
+ log.write(filePath);
163
182
  };
164
- export const readHtml = file => {
165
- const html = hfs.text(file);
183
+ export const readHtml = filePath => {
184
+ const html = hfs.text(filePath);
166
185
  const {
167
186
  type,
168
187
  amount
@@ -176,30 +195,52 @@ export const readHtml = file => {
176
195
  formatOptions
177
196
  };
178
197
  };
179
- export const writeHtml = async (file, html, options) => {
180
- const formatted = await prettier.format(html, {
198
+ export const writeHtml = async (filePath, content, options) => {
199
+ const formatted = await prettier.format(content, {
181
200
  parser: "html",
182
201
  plugins: [htmlPlugin, cssPlugin],
183
202
  tabWidth: 2,
184
203
  useTabs: false,
185
204
  ...options
186
205
  });
187
- hfs.write(file, formatted);
188
- logWrite(file);
206
+ hfs.write(filePath, formatted);
207
+ log.write(filePath);
189
208
  };
190
- export const cleanIOSAssets = (dir, prefix) => {
191
- hfs.readDir(dir).filter(file => file.startsWith(prefix) && file.endsWith(".png")).map(file => path.join(dir, file)).forEach(file => hfs.rm(file));
209
+ const cleanIOS = dir => {
210
+ 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 => {
211
+ hfs.rm(file);
212
+ });
192
213
  };
193
- export const getIOSAssetFileName = async ({
194
- name,
195
- image,
196
- width
197
- }) => {
214
+ const getImageBase64 = async (image, width) => {
215
+ if (image == null) {
216
+ return "";
217
+ }
198
218
  const buffer = await image.clone().resize(width).png({
199
219
  quality: 100
200
220
  }).toBuffer();
201
- const hash = murmurhash(buffer.toString("base64"));
202
- return `${name}-${hash}`;
221
+ return buffer.toString("base64");
222
+ };
223
+ const getFileNameSuffix = async ({
224
+ background,
225
+ brand,
226
+ brandWidth,
227
+ darkBackground,
228
+ darkBrand,
229
+ darkLogo,
230
+ logo,
231
+ logoWidth
232
+ }) => {
233
+ const [logoHash, darkLogoHash, brandHash, darkBrandHash] = await Promise.all([getImageBase64(logo, logoWidth), getImageBase64(darkLogo, logoWidth), getImageBase64(brand, brandWidth), getImageBase64(darkBrand, brandWidth)]);
234
+ const record = {
235
+ background: background.hex,
236
+ darkBackground: darkBackground?.hex ?? "",
237
+ logo: logoHash,
238
+ darkLogo: darkLogoHash,
239
+ brand: brandHash,
240
+ darkBrand: darkBrandHash
241
+ };
242
+ const stableKey = Object.keys(record).sort().map(key => record[key]).join();
243
+ return murmurhash(stableKey);
203
244
  };
204
245
  const ensureSupportedFormat = async (name, image) => {
205
246
  if (image == null) {
@@ -213,46 +254,85 @@ const ensureSupportedFormat = async (name, image) => {
213
254
  process.exit(1);
214
255
  }
215
256
  };
216
- const getAndroidResPath = (android, {
257
+ const getAndroidOutputPath = ({
258
+ android,
259
+ assetsOutputPath,
217
260
  brandHeight,
218
261
  brandWidth,
219
262
  flavor,
263
+ isExpo,
220
264
  logoHeight,
221
- logoWidth
265
+ logoWidth,
266
+ platforms
222
267
  }) => {
223
- const androidResPath = path.resolve(android.sourceDir, android.appName, "src", flavor, "res");
224
- if (!hfs.exists(androidResPath)) {
225
- log.warn(`No ${path.relative(workingPath, androidResPath)} directory found. Skipping Android assets generation…`);
226
- } else if (logoWidth > 288 || logoHeight > 288) {
227
- log.warn("Logo size exceeding 288x288dp will be cropped by Android. Skipping Android assets generation…");
228
- } else if (brandHeight > 80 || brandWidth > 200) {
229
- log.warn("Brand size exceeding 200x80dp will be cropped by Android. Skipping Android assets generation…");
230
- } else {
231
- if (logoWidth > 192 || logoHeight > 192) {
232
- log.warn(`Logo size exceeds 192x192dp. It might be cropped by Android.`);
233
- }
234
- return androidResPath;
268
+ if (!platforms.includes("android")) {
269
+ return;
235
270
  }
271
+ if (isExpo) {
272
+ return path.resolve(assetsOutputPath, "android");
273
+ }
274
+ if (android == null) {
275
+ return;
276
+ }
277
+ const androidOutputPath = path.resolve(android.sourceDir, android.appName, "src", flavor, "res");
278
+ if (!hfs.exists(androidOutputPath)) {
279
+ return log.warn(`No ${path.relative(workingPath, androidOutputPath)} directory found. Skipping Android assets generation…`);
280
+ }
281
+ if (logoWidth > 288 || logoHeight > 288) {
282
+ return log.warn("Logo size exceeding 288x288dp will be cropped by Android. Skipping Android assets generation…");
283
+ }
284
+ if (brandWidth > 200 || brandHeight > 80) {
285
+ return log.warn("Brand size exceeding 200x80dp will be cropped by Android. Skipping Android assets generation…");
286
+ }
287
+ if (logoWidth > 192 || logoHeight > 192) {
288
+ log.warn("Logo size exceeds 192x192dp. It might be cropped by Android.");
289
+ }
290
+ return androidOutputPath;
236
291
  };
237
- const getIOSProjectPath = ios => {
238
- if (ios.xcodeProject == null) {
239
- log.warn("No Xcode project found. Skipping iOS assets generation…");
292
+ const getIOSOutputPath = ({
293
+ assetsOutputPath,
294
+ ios,
295
+ isExpo,
296
+ platforms
297
+ }) => {
298
+ if (!platforms.includes("ios")) {
240
299
  return;
241
300
  }
242
- const iosProjectPath = path.resolve(ios.sourceDir, ios.xcodeProject.name).replace(/\.(xcodeproj|xcworkspace)$/, "");
243
- if (!hfs.exists(iosProjectPath)) {
244
- log.warn(`No ${path.relative(workingPath, iosProjectPath)} directory found. Skipping iOS assets generation…`);
245
- } else {
246
- return iosProjectPath;
301
+ if (isExpo) {
302
+ return path.resolve(assetsOutputPath, "ios");
303
+ }
304
+ if (ios == null) {
305
+ return;
306
+ }
307
+ if (ios.xcodeProject == null) {
308
+ return log.warn("No Xcode project found. Skipping iOS assets generation…");
309
+ }
310
+ const iosOutputPath = path.resolve(ios.sourceDir, ios.xcodeProject.name).replace(/\.(xcodeproj|xcworkspace)$/, "");
311
+ if (!hfs.exists(iosOutputPath)) {
312
+ return log.warn(`No ${path.relative(workingPath, iosOutputPath)} directory found. Skipping iOS assets generation…`);
247
313
  }
314
+ return iosOutputPath;
248
315
  };
249
- const getHtmlTemplatePath = html => {
316
+ const getHtmlTemplatePath = ({
317
+ html,
318
+ platforms
319
+ }) => {
320
+ if (!platforms.includes("web")) {
321
+ return;
322
+ }
250
323
  const htmlTemplatePath = path.resolve(workingPath, html);
251
324
  if (!hfs.exists(htmlTemplatePath)) {
252
- log.warn(`No ${path.relative(workingPath, htmlTemplatePath)} file found. Skipping HTML + CSS generation…`);
253
- } else {
254
- return htmlTemplatePath;
325
+ return log.warn(`No ${path.relative(workingPath, htmlTemplatePath)} file found. Skipping HTML + CSS generation…`);
255
326
  }
327
+ return htmlTemplatePath;
328
+ };
329
+ export const getImageHeight = (image, width) => {
330
+ if (image == null) {
331
+ return Promise.resolve(0);
332
+ }
333
+ return image.clone().resize(width).toBuffer().then(buffer => sharp(buffer).metadata()).then(({
334
+ height = 0
335
+ }) => Math.round(height));
256
336
  };
257
337
  const requireAddon = () => {
258
338
  try {
@@ -270,9 +350,12 @@ export const generate = async ({
270
350
  licenseKey,
271
351
  ...args
272
352
  }) => {
353
+ const isExpo = getExpoConfig(projectRoot, {
354
+ skipSDKVersionRequirement: true
355
+ }).exp.sdkVersion != null;
273
356
  const [nodeStringVersion = ""] = process.versions.node.split(".");
274
- const nodeVersion = parseInt(nodeStringVersion, 10);
275
- if (!isNaN(nodeVersion) && nodeVersion < 18) {
357
+ const nodeVersion = Number.parseInt(nodeStringVersion, 10);
358
+ if (!Number.isNaN(nodeVersion) && nodeVersion < 18) {
276
359
  log.error("Requires Node 18 (or higher)");
277
360
  process.exit(1);
278
361
  }
@@ -280,7 +363,7 @@ export const generate = async ({
280
363
  const darkLogoPath = args.darkLogo != null ? path.resolve(workingPath, args.darkLogo) : undefined;
281
364
  const brandPath = args.brand != null ? path.resolve(workingPath, args.brand) : undefined;
282
365
  const darkBrandPath = args.darkBrand != null ? path.resolve(workingPath, args.darkBrand) : undefined;
283
- const assetsOutputPath = args.assetsOutput != null ? path.resolve(workingPath, args.assetsOutput) : undefined;
366
+ const assetsOutputPath = path.resolve(workingPath, args.assetsOutput);
284
367
  const logo = sharp(logoPath);
285
368
  const darkLogo = darkLogoPath != null ? sharp(darkLogoPath) : undefined;
286
369
  const brand = brandPath != null ? sharp(brandPath) : undefined;
@@ -291,7 +374,7 @@ export const generate = async ({
291
374
  const darkBackground = args.darkBackground != null ? parseColor(args.darkBackground) : undefined;
292
375
  const executeAddon = brand != null || darkBackground != null || darkLogo != null || darkBrand != null;
293
376
  if (licenseKey != null && !executeAddon) {
294
- log.warn(`You specified a license key but none of the options that requires it.`);
377
+ log.warn("You specified a license key but none of the options that requires it.");
295
378
  }
296
379
  if (licenseKey == null && executeAddon) {
297
380
  const options = [brand != null ? "brand" : "", darkBackground != null ? "dark-background" : "", darkLogo != null ? "dark-logo" : "", darkBrand != null ? "dark-brand" : ""].filter(option => option !== "").map(option => `--${option}`).join(", ");
@@ -306,49 +389,48 @@ export const generate = async ({
306
389
  await ensureSupportedFormat("Dark logo", darkLogo);
307
390
  await ensureSupportedFormat("Brand", brand);
308
391
  await ensureSupportedFormat("Dark brand", darkBrand);
309
- const logoHeight = await logo.clone().resize(logoWidth).toBuffer().then(buffer => sharp(buffer).metadata()).then(({
310
- height = 0
311
- }) => Math.round(height));
312
- const brandHeight = (await brand?.clone().resize(brandWidth).toBuffer().then(buffer => sharp(buffer).metadata()).then(({
313
- height = 0
314
- }) => Math.round(height))) ?? 0;
392
+ const logoHeight = await getImageHeight(logo, logoWidth);
393
+ const brandHeight = await getImageHeight(brand, brandWidth);
315
394
  if (logoWidth < args.logoWidth) {
316
395
  log.warn(`Logo width must be a multiple of 2. It has been rounded to ${logoWidth}dp.`);
317
396
  }
318
397
  if (brandWidth < args.brandWidth) {
319
398
  log.warn(`Brand width must be a multiple of 2. It has been rounded to ${brandWidth}dp.`);
320
399
  }
321
- const androidResPath = platforms.includes("android") && android != null ? getAndroidResPath(android, {
400
+ const fileNameSuffix = await getFileNameSuffix({
401
+ background,
402
+ brand,
403
+ brandWidth,
404
+ darkBackground,
405
+ darkBrand,
406
+ darkLogo,
407
+ logo,
408
+ logoWidth
409
+ });
410
+ const androidOutputPath = getAndroidOutputPath({
411
+ android,
412
+ assetsOutputPath,
322
413
  brandHeight,
323
414
  brandWidth,
324
415
  flavor,
416
+ isExpo,
325
417
  logoHeight,
326
- logoWidth
327
- }) : undefined;
328
- const iosProjectPath = platforms.includes("ios") && ios != null ? getIOSProjectPath(ios) : undefined;
329
- const htmlTemplatePath = platforms.includes("web") ? getHtmlTemplatePath(html) : undefined;
330
- if (androidResPath != null) {
418
+ logoWidth,
419
+ platforms
420
+ });
421
+ const iosOutputPath = getIOSOutputPath({
422
+ assetsOutputPath,
423
+ ios,
424
+ isExpo,
425
+ platforms
426
+ });
427
+ const htmlTemplatePath = getHtmlTemplatePath({
428
+ html,
429
+ platforms
430
+ });
431
+ if (androidOutputPath != null) {
331
432
  log.title("🤖", "Android");
332
- const valuesPath = path.resolve(androidResPath, "values");
333
- hfs.ensureDir(valuesPath);
334
- const colorsXmlPath = path.resolve(valuesPath, "colors.xml");
335
- const colorsXmlEntry = `<color name="bootsplash_background">${background.hex}</color>`;
336
- if (hfs.exists(colorsXmlPath)) {
337
- const {
338
- root,
339
- formatOptions
340
- } = readXml(colorsXmlPath);
341
- const nextColor = parseHtml(colorsXmlEntry);
342
- const prevColor = root.querySelector('color[name="bootsplash_background"]');
343
- if (prevColor != null) {
344
- prevColor.replaceWith(nextColor);
345
- } else {
346
- root.querySelector("resources")?.appendChild(nextColor);
347
- }
348
- writeXml(colorsXmlPath, root.toString(), formatOptions);
349
- } else {
350
- writeXml(colorsXmlPath, `<resources>${colorsXmlEntry}</resources>`);
351
- }
433
+ hfs.ensureDir(androidOutputPath);
352
434
  await Promise.all([{
353
435
  ratio: 1,
354
436
  suffix: "mdpi"
@@ -368,7 +450,7 @@ export const generate = async ({
368
450
  ratio,
369
451
  suffix
370
452
  }) => {
371
- const drawableDirPath = path.resolve(androidResPath, `drawable-${suffix}`);
453
+ const drawableDirPath = path.resolve(androidOutputPath, `drawable-${suffix}`);
372
454
  hfs.ensureDir(drawableDirPath);
373
455
 
374
456
  // https://developer.android.com/develop/ui/views/launch/splash-screen#dimensions
@@ -394,44 +476,71 @@ export const generate = async ({
394
476
  }]).png({
395
477
  quality: 100
396
478
  }).toFile(filePath)).then(() => {
397
- logWrite(filePath, {
479
+ log.write(filePath, {
398
480
  width: canvasSize,
399
481
  height: canvasSize
400
482
  });
401
483
  });
402
484
  }));
485
+ if (!isExpo) {
486
+ const valuesPath = path.resolve(androidOutputPath, "values");
487
+ hfs.ensureDir(valuesPath);
488
+ const colorsXmlPath = path.resolve(valuesPath, "colors.xml");
489
+ const colorsXmlEntry = `<color name="bootsplash_background">${background.hex}</color>`;
490
+ if (hfs.exists(colorsXmlPath)) {
491
+ const {
492
+ root,
493
+ formatOptions
494
+ } = readXml(colorsXmlPath);
495
+ const nextColor = parseHtml(colorsXmlEntry);
496
+ const prevColor = root.querySelector('color[name="bootsplash_background"]');
497
+ if (prevColor != null) {
498
+ prevColor.replaceWith(nextColor);
499
+ } else {
500
+ root.querySelector("resources")?.appendChild(nextColor);
501
+ }
502
+ writeXml(colorsXmlPath, root.toString(), formatOptions);
503
+ } else {
504
+ writeXml(colorsXmlPath, `<resources>${colorsXmlEntry}</resources>`);
505
+ }
506
+ }
403
507
  }
404
- if (iosProjectPath != null) {
508
+ if (iosOutputPath != null) {
405
509
  log.title("🍏", "iOS");
406
- const storyboardPath = path.resolve(iosProjectPath, "BootSplash.storyboard");
510
+ hfs.ensureDir(iosOutputPath);
511
+ cleanIOS(iosOutputPath);
512
+ const storyboardPath = path.resolve(iosOutputPath, "BootSplash.storyboard");
513
+ const colorsSetPath = path.resolve(iosOutputPath, "Colors.xcassets", `BootSplashBackground-${fileNameSuffix}.colorset`);
514
+ const imageSetPath = path.resolve(iosOutputPath, "Images.xcassets", `BootSplashLogo-${fileNameSuffix}.imageset`);
515
+ hfs.ensureDir(colorsSetPath);
516
+ hfs.ensureDir(imageSetPath);
407
517
  writeXml(storyboardPath, getStoryboard({
408
518
  logoHeight,
409
519
  logoWidth,
410
- background: background.rgb
520
+ background: background.rgb,
521
+ fileNameSuffix
411
522
  }), {
412
523
  whiteSpaceAtEndOfSelfclosingTag: false
413
524
  });
414
- addFileToXcodeProject(path.join(path.basename(iosProjectPath), "BootSplash.storyboard"));
415
- const infoPlistPath = path.join(iosProjectPath, "Info.plist");
416
- const infoPlist = plist.parse(hfs.text(infoPlistPath));
417
- infoPlist["UILaunchStoryboardName"] = "BootSplash";
418
- const formatted = formatXml(plist.build(infoPlist), {
419
- collapseContent: true,
420
- forceSelfClosingEmptyTag: false,
421
- indentation: "\t",
422
- lineSeparator: "\n",
423
- whiteSpaceAtEndOfSelfclosingTag: false
424
- }).replace(/<string\/>/gm, "<string></string>").replace(/^\t/gm, "");
425
- hfs.write(infoPlistPath, formatted);
426
- logWrite(infoPlistPath);
427
- const imageSetPath = path.resolve(iosProjectPath, "Images.xcassets", "BootSplashLogo.imageset");
428
- hfs.ensureDir(imageSetPath);
429
- cleanIOSAssets(imageSetPath, "bootsplash_logo");
430
- const logoFileName = await getIOSAssetFileName({
431
- name: "bootsplash_logo",
432
- image: logo,
433
- width: logoWidth
525
+ writeJson(path.resolve(colorsSetPath, "Contents.json"), {
526
+ colors: [{
527
+ idiom: "universal",
528
+ color: {
529
+ "color-space": "srgb",
530
+ components: {
531
+ blue: background.rgb.B,
532
+ green: background.rgb.G,
533
+ red: background.rgb.R,
534
+ alpha: "1.000"
535
+ }
536
+ }
537
+ }],
538
+ info: {
539
+ author: "xcode",
540
+ version: 1
541
+ }
434
542
  });
543
+ const logoFileName = `logo-${fileNameSuffix}`;
435
544
  writeJson(path.resolve(imageSetPath, "Contents.json"), {
436
545
  images: [{
437
546
  idiom: "universal",
@@ -471,12 +580,44 @@ export const generate = async ({
471
580
  width,
472
581
  height
473
582
  }) => {
474
- logWrite(filePath, {
583
+ log.write(filePath, {
475
584
  width,
476
585
  height
477
586
  });
478
587
  });
479
588
  }));
589
+ if (!isExpo) {
590
+ const infoPlistPath = path.resolve(iosOutputPath, "Info.plist");
591
+ const infoPlist = plist.parse(hfs.text(infoPlistPath));
592
+ infoPlist["UILaunchStoryboardName"] = "BootSplash";
593
+ const formatted = formatXml(plist.build(infoPlist), {
594
+ collapseContent: true,
595
+ forceSelfClosingEmptyTag: false,
596
+ indentation: "\t",
597
+ lineSeparator: "\n",
598
+ whiteSpaceAtEndOfSelfclosingTag: false
599
+ }).replace(/<string\/>/gm, "<string></string>").replace(/^\t/gm, "");
600
+ hfs.write(infoPlistPath, formatted);
601
+ log.write(infoPlistPath);
602
+ const pbxprojectPath = Expo.IOSConfig.Paths.getPBXProjectPath(projectRoot);
603
+ const xcodeProjectPath = Expo.IOSConfig.Paths.getXcodeProjectPath(projectRoot);
604
+ const project = Expo.IOSConfig.XcodeUtils.getPbxproj(projectRoot);
605
+ const projectName = path.basename(iosOutputPath);
606
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
607
+ filepath: path.join(projectName, "BootSplash.storyboard"),
608
+ groupName: path.parse(xcodeProjectPath).name,
609
+ project,
610
+ isBuildFile: true
611
+ });
612
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
613
+ filepath: path.join(projectName, "Colors.xcassets"),
614
+ groupName: path.parse(xcodeProjectPath).name,
615
+ project,
616
+ isBuildFile: true
617
+ });
618
+ hfs.write(pbxprojectPath, project.writeSync());
619
+ log.write(pbxprojectPath);
620
+ }
480
621
  }
481
622
  if (htmlTemplatePath != null) {
482
623
  log.title("🌐", "Web");
@@ -531,55 +672,55 @@ export const generate = async ({
531
672
  }
532
673
  await writeHtml(htmlTemplatePath, root.toString(), formatOptions);
533
674
  }
534
- if (assetsOutputPath != null) {
535
- log.title("📄", "Assets");
536
- hfs.ensureDir(assetsOutputPath);
537
- writeJson(path.resolve(assetsOutputPath, "bootsplash_manifest.json"), {
538
- background: background.hex,
539
- logo: {
540
- width: logoWidth,
541
- height: logoHeight
542
- }
543
- });
544
- await Promise.all([{
545
- ratio: 1,
546
- suffix: ""
547
- }, {
548
- ratio: 1.5,
549
- suffix: "@1,5x"
550
- }, {
551
- ratio: 2,
552
- suffix: "@2x"
553
- }, {
554
- ratio: 3,
555
- suffix: "@3x"
556
- }, {
557
- ratio: 4,
558
- suffix: "@4x"
559
- }].map(({
560
- ratio,
561
- suffix
675
+ log.title("📄", "Assets");
676
+ hfs.ensureDir(assetsOutputPath);
677
+ writeJson(path.resolve(assetsOutputPath, "manifest.json"), {
678
+ background: background.hex,
679
+ logo: {
680
+ width: logoWidth,
681
+ height: logoHeight
682
+ }
683
+ });
684
+ await Promise.all([{
685
+ ratio: 1,
686
+ suffix: ""
687
+ }, {
688
+ ratio: 1.5,
689
+ suffix: "@1,5x"
690
+ }, {
691
+ ratio: 2,
692
+ suffix: "@2x"
693
+ }, {
694
+ ratio: 3,
695
+ suffix: "@3x"
696
+ }, {
697
+ ratio: 4,
698
+ suffix: "@4x"
699
+ }].map(({
700
+ ratio,
701
+ suffix
702
+ }) => {
703
+ const filePath = path.resolve(assetsOutputPath, `logo${suffix}.png`);
704
+ return logo.clone().resize(Math.round(logoWidth * ratio)).png({
705
+ quality: 100
706
+ }).toFile(filePath).then(({
707
+ width,
708
+ height
562
709
  }) => {
563
- const filePath = path.resolve(assetsOutputPath, `bootsplash_logo${suffix}.png`);
564
- return logo.clone().resize(Math.round(logoWidth * ratio)).png({
565
- quality: 100
566
- }).toFile(filePath).then(({
710
+ log.write(filePath, {
567
711
  width,
568
712
  height
569
- }) => {
570
- logWrite(filePath, {
571
- width,
572
- height
573
- });
574
713
  });
575
- }));
576
- }
714
+ });
715
+ }));
577
716
  if (licenseKey != null && executeAddon) {
578
717
  const addon = requireAddon();
579
718
  await addon?.execute({
580
719
  licenseKey,
581
- androidResPath,
582
- iosProjectPath,
720
+ isExpo,
721
+ fileNameSuffix,
722
+ androidOutputPath,
723
+ iosOutputPath,
583
724
  htmlTemplatePath,
584
725
  assetsOutputPath,
585
726
  logoHeight,
@@ -598,12 +739,240 @@ export const generate = async ({
598
739
  darkBrand
599
740
  });
600
741
  } else {
601
- log.text(`
742
+ console.log(`
602
743
  ${pc.blue("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")}
603
744
  ${pc.blue("┃")} 🔑 ${pc.bold("Get a license key for brand image / dark mode support")} ${pc.blue("┃")}
604
745
  ${pc.blue("┃")} ${pc.underline("https://zoontek.gumroad.com/l/bootsplash-generator")} ${pc.blue("┃")}
605
746
  ${pc.blue("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")}`);
606
747
  }
607
- log.text(`\n💖 Thanks for using ${pc.underline("react-native-bootsplash")}`);
748
+ console.log(`\n💖 Thanks for using ${pc.underline("react-native-bootsplash")}`);
749
+ };
750
+ const withAndroidAssets = (config, props) => Expo.withDangerousMod(config, ["android", config => {
751
+ const {
752
+ assetsDir = "assets/bootsplash"
753
+ } = props;
754
+ const {
755
+ platformProjectRoot
756
+ } = config.modRequest;
757
+ const srcDir = path.resolve(workingPath, assetsDir, "android");
758
+ const destDir = path.resolve(platformProjectRoot, "app", "src", "main", "res");
759
+ for (const drawableDir of hfs.readDir(srcDir)) {
760
+ const srcDrawableDir = path.join(srcDir, drawableDir);
761
+ const destDrawableDir = path.join(destDir, drawableDir);
762
+ hfs.ensureDir(destDrawableDir);
763
+ for (const file of hfs.readDir(srcDrawableDir)) {
764
+ hfs.copy(path.join(srcDrawableDir, file), path.join(destDrawableDir, file));
765
+ }
766
+ }
767
+ return config;
768
+ }]);
769
+ const withAndroidManifest = config => Expo.withAndroidManifest(config, config => {
770
+ config.modResults.manifest.application?.forEach(application => {
771
+ if (application.$["android:name"] === ".MainApplication") {
772
+ const {
773
+ activity
774
+ } = application;
775
+ activity?.forEach(activity => {
776
+ if (activity.$["android:name"] === ".MainActivity") {
777
+ activity.$["android:theme"] = "@style/BootTheme";
778
+ }
779
+ });
780
+ }
781
+ });
782
+ return config;
783
+ });
784
+ const withMainActivity = config => Expo.withMainActivity(config, config => {
785
+ const {
786
+ modResults
787
+ } = config;
788
+ const {
789
+ language
790
+ } = modResults;
791
+ const withImports = addImports(modResults.contents.replace(/(\/\/ )?setTheme\(R\.style\.AppTheme\)/, "// setTheme(R.style.AppTheme)"), ["android.os.Bundle", "com.zoontek.rnbootsplash.RNBootSplash"], language === "java");
792
+
793
+ // indented with 4 spaces
794
+ const withInit = mergeContents({
795
+ src: withImports,
796
+ comment: " //",
797
+ tag: "bootsplash-init",
798
+ offset: 0,
799
+ anchor: /super\.onCreate\(null\)/,
800
+ newSrc: " RNBootSplash.init(this, R.style.BootTheme)" + (language === "java" ? ";" : "")
801
+ });
802
+ return {
803
+ ...config,
804
+ modResults: {
805
+ ...modResults,
806
+ contents: withInit.contents
807
+ }
808
+ };
809
+ });
810
+ const withAndroidStyles = (config, props) => Expo.withAndroidStyles(config, async config => {
811
+ const {
812
+ assetsDir = "assets/bootsplash",
813
+ edgeToEdge = false
814
+ } = props;
815
+ const manifest = await hfs.json(path.resolve(workingPath, assetsDir, "manifest.json"));
816
+ const item = [{
817
+ $: {
818
+ name: "postBootSplashTheme"
819
+ },
820
+ _: "@style/AppTheme"
821
+ }, {
822
+ $: {
823
+ name: "bootSplashBackground"
824
+ },
825
+ _: "@color/bootsplash_background"
826
+ }, {
827
+ $: {
828
+ name: "bootSplashLogo"
829
+ },
830
+ _: "@drawable/bootsplash_logo"
831
+ }];
832
+ if (manifest.brand != null) {
833
+ item.push({
834
+ $: {
835
+ name: "bootSplashBrand"
836
+ },
837
+ _: "@drawable/bootsplash_brand"
838
+ });
839
+ }
840
+ config.modResults.resources.style?.filter(({
841
+ $
842
+ }) => $.name !== "BootTheme").push({
843
+ item,
844
+ $: {
845
+ name: "BootTheme",
846
+ parent: edgeToEdge ? "Theme.BootSplash" : "Theme.BootSplash.EdgeToEdge"
847
+ }
848
+ });
849
+ return config;
850
+ });
851
+ const withAndroidColors = (config, props) => Expo.withAndroidColors(config, async config => {
852
+ const {
853
+ assetsDir = "assets/bootsplash"
854
+ } = props;
855
+ const manifest = await hfs.json(path.resolve(workingPath, assetsDir, "manifest.json"));
856
+ config.modResults = assignColorValue(config.modResults, {
857
+ name: "bootsplash_background",
858
+ value: manifest.background
859
+ });
860
+ return config;
861
+ });
862
+ const withAndroidColorsNight = (config, props) => Expo.withAndroidColorsNight(config, async config => {
863
+ const {
864
+ assetsDir = "assets/bootsplash"
865
+ } = props;
866
+ const manifest = await hfs.json(path.resolve(workingPath, assetsDir, "manifest.json"));
867
+ if (manifest.darkBackground != null) {
868
+ config.modResults = assignColorValue(config.modResults, {
869
+ name: "bootsplash_background",
870
+ value: manifest.darkBackground
871
+ });
872
+ }
873
+ return config;
874
+ });
875
+ const withIOSAssets = (config, props) => Expo.withDangerousMod(config, ["ios", config => {
876
+ const {
877
+ assetsDir = "assets/bootsplash"
878
+ } = props;
879
+ const {
880
+ platformProjectRoot,
881
+ projectName = ""
882
+ } = config.modRequest;
883
+ const srcDir = path.resolve(workingPath, assetsDir, "ios");
884
+ const destDir = path.resolve(platformProjectRoot, projectName);
885
+ cleanIOS(destDir);
886
+ hfs.copy(path.join(srcDir, "BootSplash.storyboard"), path.join(destDir, "BootSplash.storyboard"));
887
+ for (const xcassetsDir of ["Colors.xcassets", "Images.xcassets"]) {
888
+ const srcXcassetsDir = path.join(srcDir, xcassetsDir);
889
+ const destXcassetsDir = path.join(destDir, xcassetsDir);
890
+ hfs.ensureDir(destXcassetsDir);
891
+ for (const file of hfs.readDir(srcXcassetsDir)) {
892
+ hfs.copy(path.join(srcXcassetsDir, file), path.join(destXcassetsDir, file));
893
+ }
894
+ }
895
+ return config;
896
+ }]);
897
+ const withAppDelegate = config => Expo.withAppDelegate(config, config => {
898
+ const [sdkStringVersion = ""] = config.sdkVersion?.split(".") ?? "";
899
+ const isAtLeastExpo51 = Number.parseInt(sdkStringVersion, 10) >= 51;
900
+ const {
901
+ modResults
902
+ } = config;
903
+ const {
904
+ language
905
+ } = modResults;
906
+ if (language !== "objc" && language !== "objcpp") {
907
+ throw new Error(`Cannot modify the project AppDelegate as it's not in a supported language: ${language}`);
908
+ }
909
+ const withHeader = mergeContents({
910
+ src: modResults.contents,
911
+ comment: "//",
912
+ tag: "bootsplash-header",
913
+ offset: 1,
914
+ anchor: /#import "AppDelegate\.h"/,
915
+ newSrc: '#import "RNBootSplash.h"'
916
+ });
917
+ const withRootView = mergeContents({
918
+ src: withHeader.contents,
919
+ comment: "//",
920
+ tag: "bootsplash-init",
921
+ offset: 0,
922
+ anchor: /@end/,
923
+ newSrc: isAtLeastExpo51 ? dedent`
924
+ - (void)customizeRootView:(RCTRootView *)rootView {
925
+ [RNBootSplash initWithStoryboard:@"BootSplash" rootView:rootView];
926
+ }
927
+ ` : dedent`
928
+ - (UIView *)createRootViewWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName initProps:(NSDictionary *)initProps {
929
+ UIView *rootView = [super createRootViewWithBridge:bridge moduleName:moduleName initProps:initProps];
930
+ [RNBootSplash initWithStoryboard:@"BootSplash" rootView:rootView];
931
+ return rootView;
932
+ }
933
+ `
934
+ });
935
+ return {
936
+ ...config,
937
+ modResults: {
938
+ ...modResults,
939
+ contents: withRootView.contents
940
+ }
941
+ };
942
+ });
943
+ const withInfoPlist = config => Expo.withInfoPlist(config, config => {
944
+ config.modResults["UILaunchStoryboardName"] = "BootSplash";
945
+ return config;
946
+ });
947
+ const withXcodeProject = config => Expo.withXcodeProject(config, config => {
948
+ const {
949
+ projectName = ""
950
+ } = config.modRequest;
951
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
952
+ filepath: path.join(projectName, "BootSplash.storyboard"),
953
+ groupName: projectName,
954
+ project: config.modResults,
955
+ isBuildFile: true
956
+ });
957
+ Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
958
+ filepath: path.join(projectName, "Colors.xcassets"),
959
+ groupName: projectName,
960
+ project: config.modResults,
961
+ isBuildFile: true
962
+ });
963
+ return config;
964
+ });
965
+ export const withGenerate = (config, props = {}) => {
966
+ const plugins = [];
967
+ const {
968
+ platforms = []
969
+ } = config;
970
+ if (platforms.includes("android")) {
971
+ plugins.push(withAndroidAssets, withAndroidManifest, withMainActivity, withAndroidStyles, withAndroidColors, withAndroidColorsNight);
972
+ }
973
+ if (platforms.includes("ios")) {
974
+ plugins.push(withIOSAssets, withAppDelegate, withInfoPlist, withXcodeProject);
975
+ }
976
+ return Expo.withPlugins(config, plugins.map(plugin => [plugin, props]));
608
977
  };
609
978
  //# sourceMappingURL=generate.js.map