react-native-bootsplash-screen
Version:
Display a bootsplash on your app starts. Hide it when you want.
403 lines (345 loc) • 12.1 kB
JavaScript
"use strict";
const path = require("path");
const fs = require("fs");
const chalk = require("chalk");
const jimp = require("jimp");
const prompts = require("prompts");
let projectName;
const logoFileName = "bootsplash_logo";
const xcassetName = "BootSplashLogo";
const androidColorRegex = /<color name="bootsplash_background">#\w+<\/color>/g;
const initialProjectPath = path.join(
".",
path.relative(
process.cwd(),
path.resolve(path.join(__dirname, "..", "..", "..")),
),
);
const ContentsJson = `{
"images": [
{
"idiom": "universal",
"filename": "${logoFileName}.png",
"scale": "1x"
},
{
"idiom": "universal",
"filename": "${logoFileName}@2x.png",
"scale": "2x"
},
{
"idiom": "universal",
"filename": "${logoFileName}@3x.png",
"scale": "3x"
}
],
"info": {
"version": 1,
"author": "xcode"
}
}
`;
const getStoryboard = ({ height, width, r, g, b }) => {
const x = (414 - width) / 2;
const y = (896 - height) / 2;
return `<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Dtp-p8-LvN">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="Fnd-62-7zz">
<objects>
<viewController id="Dtp-p8-LvN" sceneMemberID="viewController">
<view key="view" autoresizesSubviews="NO" userInteractionEnabled="NO" contentMode="scaleToFill" id="guO-oA-Nhw">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView autoresizesSubviews="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="${xcassetName}" translatesAutoresizingMaskIntoConstraints="NO" id="3lX-Ut-9ad">
<rect key="frame" x="${x}" y="${y}" width="${width}" height="${height}"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" image="YES" notEnabled="YES"/>
</accessibility>
</imageView>
</subviews>
<color key="backgroundColor" red="${r}" green="${g}" blue="${b}" alpha="1" colorSpace="calibratedRGB"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" notEnabled="YES"/>
</accessibility>
<constraints>
<constraint firstItem="3lX-Ut-9ad" firstAttribute="centerX" secondItem="eg9-kz-Dhh" secondAttribute="centerX" id="Fh9-Fy-1nT"/>
<constraint firstItem="3lX-Ut-9ad" firstAttribute="centerY" secondItem="guO-oA-Nhw" secondAttribute="centerY" id="nvB-Ic-PnI"/>
</constraints>
<viewLayoutGuide key="safeArea" id="eg9-kz-Dhh"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Lvb-Jr-bCV" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="0.0"/>
</scene>
</scenes>
<resources>
<image name="${xcassetName}" width="${width}" height="${height}"/>
</resources>
</document>
`;
};
const drawableXml = `<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
<item android:drawable="@color/bootsplash_background" />
<item>
<bitmap android:src="@mipmap/${logoFileName}" android:gravity="center" />
</item>
</layer-list>
`;
const log = (text, dim = false) => {
console.log(dim ? chalk.dim(text) : text);
};
const ensureDir = (dir) => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
};
const isValidHexadecimal = (value) => /^#?([0-9A-F]{3}){1,2}$/i.test(value);
const toFullHexadecimal = (hex) => {
const prefixed = hex[0] === "#" ? hex : `#${hex}`;
const up = prefixed.toUpperCase();
return up.length === 4
? "#" + up[1] + up[1] + up[2] + up[2] + up[3] + up[3]
: up;
};
const hexadecimalToAppleColor = (hex) => ({
r: (parseInt(hex[1] + hex[2], 16) / 255).toPrecision(15),
g: (parseInt(hex[3] + hex[4], 16) / 255).toPrecision(15),
b: (parseInt(hex[5] + hex[6], 16) / 255).toPrecision(15),
});
const getProjectName = (projectPath) => {
try {
const appJsonPath = path.join(projectPath, "app.json");
const appJson = fs.readFileSync(appJsonPath, "utf-8");
const { name } = JSON.parse(appJson);
if (!name) {
throw new Error("Invalid projectPath");
}
return name;
} catch (e) {
return false;
}
};
const questions = [
{
name: "projectPath",
type: "text",
initial: initialProjectPath,
message: "The path to the root of your React Native project",
validate: (value) => {
if (!fs.existsSync(value)) {
return `Invalid project path. The directory ${chalk.bold(
value,
)} could not be found.`;
}
projectName = getProjectName(value);
if (!projectName) {
return `Invalid React Native project. A valid ${chalk.bold(
"app.json",
)} file could not be found.`;
}
return true;
},
},
{
name: "assetsPath",
type: "text",
initial: (prev) => path.join(prev, "assets"),
message: "The path to your static assets directory",
validate: (value) => {
if (!fs.existsSync(value)) {
return `Invalid assets path. The directory ${chalk.bold(
value,
)} could not be found.`;
}
return true;
},
},
{
name: "iconPath",
type: "text",
message: "Your original icon file",
initial: (prev) => path.join(prev, `${logoFileName}_original.png`),
validate: (value) => {
if (!fs.existsSync(value)) {
return `Invalid icon file path. The file ${chalk.bold(
value,
)} could not be found.`;
}
return true;
},
},
{
name: "backgroundColor",
type: "text",
message: "The bootsplash background color (in hexadecimal)",
initial: "#FFF",
validate: (value) => {
if (!isValidHexadecimal(value)) {
return "Invalid hexadecimal color.";
}
return true;
},
},
{
name: "iconWidth",
type: "number",
message: "The desired icon width (in dp - we recommend approximately ~100)",
initial: 100,
min: 1,
max: 1000,
},
{
name: "confirmation",
type: "confirm",
message:
"Are you sure? All the existing bootsplash images will be overwritten!",
initial: true,
},
];
async function generate({
projectPath,
assetsPath,
iconPath,
backgroundColor,
iconWidth: w1,
confirmation,
}) {
if (!projectPath || !assetsPath || !iconPath || !w1 || !confirmation) {
process.exit(1);
}
const image = await jimp.read(iconPath);
const imageMap = [];
const fullHexadecimal = toFullHexadecimal(backgroundColor);
const appleColors = hexadecimalToAppleColor(fullHexadecimal);
const h = (size) =>
Math.ceil(size * (image.bitmap.height / image.bitmap.width));
const w15 = w1 * 1.5;
const w2 = w1 * 2;
const w3 = w1 * 3;
const w4 = w1 * 4;
const androidResPath = path.join(
projectPath,
"android",
"app",
"src",
"main",
"res",
);
if (fs.existsSync(androidResPath)) {
const fileName = `${logoFileName}.png`;
imageMap.push(
[path.join(androidResPath, "mipmap-mdpi", fileName), [w1, h(w1)]],
[path.join(androidResPath, "mipmap-hdpi", fileName), [w15, h(w15)]],
[path.join(androidResPath, "mipmap-xhdpi", fileName), [w2, h(w2)]],
[path.join(androidResPath, "mipmap-xxhdpi", fileName), [w3, h(w3)]],
[path.join(androidResPath, "mipmap-xxxhdpi", fileName), [w4, h(w4)]],
);
} else {
log(`No ${androidResPath} directory found. Skipping android generation…`);
}
const iosProjectPath = path.join(projectPath, "ios", projectName);
const iosImagesPath = path.join(iosProjectPath, "Images.xcassets");
if (fs.existsSync(iosImagesPath)) {
const iosImageSetPath = path.join(iosImagesPath, `${xcassetName}.imageset`);
ensureDir(iosImageSetPath);
fs.writeFileSync(
path.join(iosImageSetPath, "Contents.json"),
ContentsJson,
"utf-8",
);
imageMap.push(
[path.join(iosImageSetPath, `${logoFileName}.png`), [w1, h(w1)]],
[path.join(iosImageSetPath, `${logoFileName}@2x.png`), [w2, h(w2)]],
[path.join(iosImageSetPath, `${logoFileName}@3x.png`), [w3, h(w3)]],
);
} else {
log(`No ${iosImagesPath} directory found. Skipping iOS generation…`);
}
imageMap.push(
[path.join(assetsPath, logoFileName + ".png"), [w1, h(w1)]],
[path.join(assetsPath, logoFileName + "@1,5x.png"), [w15, h(w15)]],
[path.join(assetsPath, logoFileName + "@2x.png"), [w2, h(w2)]],
[path.join(assetsPath, logoFileName + "@3x.png"), [w3, h(w3)]],
[path.join(assetsPath, logoFileName + "@4x.png"), [w4, h(w4)]],
);
log("👍 Looking good! Generating files…");
await Promise.all(
imageMap.map(([path, [width, height]]) =>
image
.clone()
.cover(width, height)
.writeAsync(path)
.then(() => {
log(`✨ ${path} (${width}x${height})`, true);
}),
),
);
if (fs.existsSync(iosProjectPath)) {
const storyboard = path.join(iosProjectPath, `BootSplash.storyboard`);
fs.writeFileSync(
storyboard,
getStoryboard({ height: h(w1), width: w1, ...appleColors }),
"utf-8",
);
log(`✨ ${storyboard}`, true);
}
if (fs.existsSync(androidResPath)) {
const drawableDir = path.join(androidResPath, "drawable");
ensureDir(drawableDir);
const drawable = path.join(drawableDir, "bootsplash.xml");
fs.writeFileSync(drawable, drawableXml, "utf-8");
log(`✨ ${drawable}`, true);
const valuesDir = path.join(androidResPath, "values");
ensureDir(valuesDir);
const colors = path.join(valuesDir, "colors.xml");
if (fs.existsSync(colors)) {
const content = fs.readFileSync(colors, "utf-8");
if (content.match(androidColorRegex)) {
fs.writeFileSync(
colors,
content.replace(
androidColorRegex,
`<color name="bootsplash_background">${fullHexadecimal}</color>`,
),
"utf-8",
);
} else {
fs.writeFileSync(
colors,
content.replace(
/<\/resources>/g,
` <color name="bootsplash_background">${fullHexadecimal}</color>\n</resources>`,
),
"utf-8",
);
}
log(`✏️ Editing ${colors}`, true);
} else {
fs.writeFileSync(
colors,
`<resources>\n <color name="bootsplash_background">${fullHexadecimal}</color>\n</resources>\n`,
"utf-8",
);
log(`✨ ${colors}`, true);
}
}
log(
`✅ Done! Thanks for using ${chalk.underline("react-native-bootsplash")}.`,
);
}
prompts(questions)
.then(generate)
.catch((error) => log(chalk.red.bold(error.toString())));