react-native-bootsplash
Version:
Display a bootsplash on your app starts. Hide it when you want.
877 lines (872 loc) • 32.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.writeXmlLike = exports.writeJson = exports.readXmlLike = exports.log = exports.hfs = exports.getExpoConfig = exports.generate = exports.cleanIOSAssets = void 0;
var Expo = _interopRequireWildcard(require("@expo/config-plugins"));
var _plist = _interopRequireDefault(require("@expo/plist"));
var _cliConfigAndroid = require("@react-native-community/cli-config-android");
var _cliConfigApple = require("@react-native-community/cli-config-apple");
var _cliTools = require("@react-native-community/cli-tools");
var _child_process = _interopRequireDefault(require("child_process"));
var _crypto = _interopRequireDefault(require("crypto"));
var _detectIndent = _interopRequireDefault(require("detect-indent"));
var _fsExtra = _interopRequireDefault(require("fs-extra"));
var _nodeHtmlParser = require("node-html-parser");
var _path = _interopRequireDefault(require("path"));
var _picocolors = _interopRequireDefault(require("picocolors"));
var htmlPlugin = _interopRequireWildcard(require("prettier/plugins/html"));
var cssPlugin = _interopRequireWildcard(require("prettier/plugins/postcss"));
var prettier = _interopRequireWildcard(require("prettier/standalone"));
var _semver = _interopRequireDefault(require("semver"));
var _sharp = _interopRequireDefault(require("sharp"));
var _tsDedent = require("ts-dedent");
var _util = _interopRequireDefault(require("util"));
var _xmlFormatter = _interopRequireDefault(require("xml-formatter"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
const workingPath = process.env.INIT_CWD ?? process.env.PWD ?? process.cwd();
const projectRoot = (0, _cliTools.findProjectRoot)(workingPath);
const getIOSProjectConfig = (0, _cliConfigApple.getProjectConfig)({
platformName: "ios"
});
const ios = getIOSProjectConfig(projectRoot, {});
const android = (0, _cliConfigAndroid.projectConfig)(projectRoot);
const promisifiedExec = _util.default.promisify(_child_process.default.exec);
const exec = cmd => promisifiedExec(cmd).then(({
stdout,
stderr
}) => stdout || stderr);
const log = exports.log = {
error: text => {
console.log(_picocolors.default.red(`❌ ${text}`));
},
title: (emoji, text) => {
console.log(`\n${emoji} ${_picocolors.default.underline(_picocolors.default.bold(text))}`);
},
warn: text => {
console.log(_picocolors.default.yellow(`⚠️ ${text}`));
},
write: (filePath, dimensions) => {
console.log(` ${_path.default.relative(workingPath, filePath)}` + (dimensions != null ? ` (${dimensions.width}x${dimensions.height})` : ""));
}
};
const parseColor = value => {
const up = value.toUpperCase().replace(/[^0-9A-F]/g, "");
if (up.length !== 3 && up.length !== 6) {
log.error(`"${value}" value is not a valid hexadecimal color.`);
process.exit(1);
}
const hex = up.length === 3 ? "#" + up[0] + up[0] + up[1] + up[1] + up[2] + up[2] : "#" + up;
const rgb = {
R: (Number.parseInt("" + hex[1] + hex[2], 16) / 255).toPrecision(15),
G: (Number.parseInt("" + hex[3] + hex[4], 16) / 255).toPrecision(15),
B: (Number.parseInt("" + hex[5] + hex[6], 16) / 255).toPrecision(15)
};
return {
hex: hex.toLowerCase(),
rgb
};
};
const getStoryboard = ({
logoHeight,
logoWidth,
background: {
R,
G,
B
},
fileNameSuffix
}) => {
const frameWidth = 375;
const frameHeight = 667;
const logoX = (frameWidth - logoWidth) / 2;
const logoY = (frameHeight - logoHeight) / 2;
return (0, _tsDedent.dedent)`
xml version="1.0" encoding="UTF-8"
<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">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<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="EHf-IW-A2E">
<objects>
<viewController modalTransitionStyle="crossDissolve" id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" autoresizesSubviews="NO" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="${frameWidth}" height="${frameHeight}"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView autoresizesSubviews="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="BootSplashLogo-${fileNameSuffix}" translatesAutoresizingMaskIntoConstraints="NO" id="3lX-Ut-9ad">
<rect key="frame" x="${logoX}" y="${logoY}" width="${logoWidth}" height="${logoHeight}"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" image="YES" notEnabled="YES"/>
</accessibility>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
<color key="backgroundColor" name="BootSplashBackground-${fileNameSuffix}"/>
<constraints>
<constraint firstItem="3lX-Ut-9ad" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="Fh9-Fy-1nT"/>
<constraint firstItem="3lX-Ut-9ad" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="nvB-Ic-PnI"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="0.0"/>
</scene>
</scenes>
<resources>
<image name="BootSplashLogo-${fileNameSuffix}" width="${logoWidth}" height="${logoHeight}"/>
<namedColor name="BootSplashBackground-${fileNameSuffix}">
<color red="${R}" green="${G}" blue="${B}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>
`;
};
// Freely inspired by https://github.com/humanwhocodes/humanfs
const hfs = exports.hfs = {
buffer: path => _fsExtra.default.readFileSync(path),
exists: path => _fsExtra.default.existsSync(path),
isDir: path => _fsExtra.default.lstatSync(path).isDirectory(),
json: path => JSON.parse(_fsExtra.default.readFileSync(path, "utf-8")),
readDir: path => _fsExtra.default.readdirSync(path, "utf-8"),
realPath: path => _fsExtra.default.realpathSync(path, "utf-8"),
rm: path => _fsExtra.default.rmSync(path, {
force: true,
recursive: true
}),
text: path => _fsExtra.default.readFileSync(path, "utf-8"),
copy: (src, dest) => {
if (hfs.isDir(src) || !hfs.exists(dest)) {
return _fsExtra.default.copySync(src, dest, {
overwrite: true
});
}
const srcBuffer = _fsExtra.default.readFileSync(src);
const destBuffer = _fsExtra.default.readFileSync(dest);
if (!srcBuffer.equals(destBuffer)) {
return _fsExtra.default.copySync(src, dest, {
overwrite: true
});
}
},
ensureDir: dir => {
_fsExtra.default.mkdirSync(dir, {
recursive: true
});
},
write: (path, content) => {
const trimmed = content.trim();
_fsExtra.default.writeFileSync(path, trimmed === "" ? trimmed : trimmed + "\n", "utf-8");
}
};
// Adapted from https://github.com/square/find-yarn-workspace-root
const findUp = (from, matcher) => {
let previous;
let current = _path.default.normalize(from);
do {
const found = matcher(current);
if (typeof found !== "undefined") {
return found;
}
previous = current;
current = _path.default.dirname(current);
} while (current !== previous);
};
const getExpoConfig = from => {
const hasDependency = findUp(from, dir => {
const pkgPath = _path.default.resolve(dir, "package.json");
if (_fsExtra.default.existsSync(pkgPath)) {
try {
const pkg = hfs.json(pkgPath);
return pkg.dependencies?.expo != null;
} catch {} // eslint-disable-line no-empty
}
}) ?? false;
if (!hasDependency) {
return {
isExpo: false
};
}
const version = findUp(from, dir => {
const pkgPath = _path.default.resolve(dir, "node_modules", "expo", "package.json");
if (_fsExtra.default.existsSync(pkgPath)) {
try {
const pkg = hfs.json(pkgPath);
return pkg.version;
} catch {} // eslint-disable-line no-empty
}
});
if (version == null || _semver.default.lt(version, "51.0.20")) {
log.error("Requires Expo 51.0.20 (or higher)");
process.exit(1);
}
return {
isExpo: true
};
};
exports.getExpoConfig = getExpoConfig;
const writeJson = (filePath, content) => {
hfs.write(filePath, JSON.stringify(content, null, 2));
log.write(filePath);
};
exports.writeJson = writeJson;
const readXmlLike = filePath => {
const content = hfs.text(filePath);
return {
root: (0, _nodeHtmlParser.parse)(content),
formatOptions: {
indent: (0, _detectIndent.default)(content)
}
};
};
exports.readXmlLike = readXmlLike;
const writeXmlLike = async (filePath, content, {
indent,
...formatOptions
}) => {
if (formatOptions.formatter === "prettier") {
const {
formatter,
useCssPlugin = false,
selfClosingTags = false,
...options
} = formatOptions;
const formatted = await prettier.format(content, {
parser: "html",
bracketSameLine: true,
printWidth: 10000,
plugins: [htmlPlugin, ...(useCssPlugin ? [cssPlugin] : [])],
useTabs: indent?.type === "tab",
tabWidth: (indent?.amount ?? 0) || 2,
...options
});
hfs.write(filePath, selfClosingTags ? formatted.replace(/><\/[a-z-0-9]+>/gi, " />") : formatted);
log.write(filePath);
} else {
const {
formatter,
...options
} = formatOptions;
const formatted = (0, _xmlFormatter.default)(content, {
collapseContent: true,
forceSelfClosingEmptyTag: true,
lineSeparator: "\n",
whiteSpaceAtEndOfSelfclosingTag: true,
indentation: (indent?.indent ?? "") || " ",
...options
});
hfs.write(filePath, formatted);
log.write(filePath);
}
};
exports.writeXmlLike = writeXmlLike;
const cleanIOSAssets = dir => {
hfs.readDir(dir).filter(file => file === "Colors.xcassets" || file === "Images.xcassets").map(file => _path.default.join(dir, file)).flatMap(dir => hfs.readDir(dir).filter(file => file.startsWith("BootSplash")).map(file => _path.default.join(dir, file))).forEach(file => {
hfs.rm(file);
});
};
exports.cleanIOSAssets = cleanIOSAssets;
const getImageBase64 = async (image, width) => {
if (image == null) {
return "";
}
const buffer = await image.clone().resize(width).png({
quality: 100
}).toBuffer();
return buffer.toString("base64");
};
const getFileNameSuffix = async ({
background,
brand,
brandWidth,
darkBackground,
darkBrand,
darkLogo,
logo,
logoWidth
}) => {
const [logoHash, darkLogoHash, brandHash, darkBrandHash] = await Promise.all([getImageBase64(logo, logoWidth), getImageBase64(darkLogo, logoWidth), getImageBase64(brand, brandWidth), getImageBase64(darkBrand, brandWidth)]);
const record = {
background: background.hex,
darkBackground: darkBackground?.hex ?? "",
logo: logoHash,
darkLogo: darkLogoHash,
brand: brandHash,
darkBrand: darkBrandHash
};
const stableKey = Object.keys(record).sort().map(key => record[key]).join();
return _crypto.default.createHash("shake256", {
outputLength: 3
}).update(stableKey).digest("hex").toLowerCase();
};
const ensureSupportedFormat = async (name, image) => {
if (image == null) {
return;
}
const {
format
} = await image.metadata();
if (format !== "png" && format !== "svg") {
log.error(`${name} image file format (${format}) is not supported`);
process.exit(1);
}
};
const getAndroidOutputPath = ({
assetsOutputPath,
brandHeight,
brandWidth,
flavor,
isExpo,
logoHeight,
logoWidth,
platforms
}) => {
if (!platforms.includes("android")) {
return;
}
const withSizeChecks = assetsOutputPath => {
if (logoWidth > 288 || logoHeight > 288) {
return log.warn("Logo size exceeding 288x288dp will be cropped by Android. Skipping Android assets generation…");
}
if (brandWidth > 200 || brandHeight > 80) {
return log.warn("Brand size exceeding 200x80dp will be cropped by Android. Skipping Android assets generation…");
}
if (logoWidth > 192 || logoHeight > 192) {
log.warn("Logo size exceeds 192x192dp. It might be cropped by Android.");
}
return assetsOutputPath;
};
if (isExpo) {
return withSizeChecks(_path.default.resolve(assetsOutputPath, "android"));
}
if (android == null) {
return;
}
const androidOutputPath = _path.default.resolve(android.sourceDir, android.appName, "src", flavor, "res");
if (!hfs.exists(androidOutputPath)) {
return log.warn(`No ${_path.default.relative(workingPath, androidOutputPath)} directory found. Skipping Android assets generation…`);
}
return withSizeChecks(androidOutputPath);
};
const getIOSOutputPath = ({
assetsOutputPath,
isExpo,
platforms
}) => {
if (!platforms.includes("ios")) {
return;
}
if (isExpo) {
return _path.default.resolve(assetsOutputPath, "ios");
}
if (ios == null) {
return;
}
if (ios.xcodeProject == null) {
return log.warn("No Xcode project found. Skipping iOS assets generation…");
}
const iosOutputPath = _path.default.resolve(ios.sourceDir, ios.xcodeProject.name).replace(/\.(xcodeproj|xcworkspace)$/, "");
if (!hfs.exists(iosOutputPath)) {
return log.warn(`No ${_path.default.relative(workingPath, iosOutputPath)} directory found. Skipping iOS assets generation…`);
}
return iosOutputPath;
};
const getHtmlTemplatePath = async ({
isExpo,
html,
platforms
}) => {
if (!platforms.includes("web")) {
return;
}
if (isExpo) {
const htmlTemplatePath = _path.default.resolve(workingPath, html);
const htmlTemplateRelativePath = _path.default.relative(workingPath, htmlTemplatePath);
if (htmlTemplateRelativePath === "public/index.html" && !hfs.exists(htmlTemplatePath)) {
const cmd = `npx expo customize ${htmlTemplateRelativePath}`;
console.log(_picocolors.default.dim(`Running ${cmd}`));
await exec(cmd);
}
}
const htmlTemplatePath = _path.default.resolve(workingPath, html);
if (!hfs.exists(htmlTemplatePath)) {
return log.warn(`No ${_path.default.relative(workingPath, htmlTemplatePath)} file found. Skipping HTML + CSS generation…`);
}
return htmlTemplatePath;
};
const getImageHeight = (image, width) => {
if (image == null) {
return Promise.resolve(0);
}
return image.clone().resize(width).toBuffer().then(buffer => (0, _sharp.default)(buffer).metadata()).then(({
height = 0
}) => Math.round(height));
};
const requireAddon = () => {
try {
return require("./addon"); // eslint-disable-line
} catch {
return;
}
};
const generate = async ({
projectType,
platforms,
html,
flavor,
licenseKey,
...args
}) => {
const isExpo = projectType === "expo" || projectType === "detect" && getExpoConfig(workingPath).isExpo;
if (_semver.default.lt(process.versions.node, "18.0.0")) {
log.error("Requires Node 18 (or higher)");
process.exit(1);
}
const logoPath = _path.default.resolve(workingPath, args.logo);
const darkLogoPath = args.darkLogo != null ? _path.default.resolve(workingPath, args.darkLogo) : undefined;
const brandPath = args.brand != null ? _path.default.resolve(workingPath, args.brand) : undefined;
const darkBrandPath = args.darkBrand != null ? _path.default.resolve(workingPath, args.darkBrand) : undefined;
const assetsOutputPath = _path.default.resolve(workingPath, args.assetsOutput);
const logo = (0, _sharp.default)(logoPath);
const darkLogo = darkLogoPath != null ? (0, _sharp.default)(darkLogoPath) : undefined;
const brand = brandPath != null ? (0, _sharp.default)(brandPath) : undefined;
const darkBrand = darkBrandPath != null ? (0, _sharp.default)(darkBrandPath) : undefined;
const background = parseColor(args.background);
const logoWidth = args.logoWidth - args.logoWidth % 2;
const brandWidth = args.brandWidth - args.brandWidth % 2;
const darkBackground = args.darkBackground != null ? parseColor(args.darkBackground) : undefined;
const executeAddon = brand != null || darkBackground != null || darkLogo != null || darkBrand != null;
if (licenseKey != null && !executeAddon) {
log.warn("You specified a license key but none of the options that requires it.");
}
if (licenseKey == null && executeAddon) {
const options = [brand != null ? "brand" : "", darkBackground != null ? "dark-background" : "", darkLogo != null ? "dark-logo" : "", darkBrand != null ? "dark-brand" : ""].filter(option => option !== "").map(option => `--${option}`).join(", ");
log.error(`You need to specify a license key in order to use ${options}.`);
process.exit(1);
}
if (brand == null && darkBrand != null) {
log.error("--dark-brand option couldn't be used without --brand.");
process.exit(1);
}
await ensureSupportedFormat("Logo", logo);
await ensureSupportedFormat("Dark logo", darkLogo);
await ensureSupportedFormat("Brand", brand);
await ensureSupportedFormat("Dark brand", darkBrand);
const logoHeight = await getImageHeight(logo, logoWidth);
const brandHeight = await getImageHeight(brand, brandWidth);
if (logoWidth < args.logoWidth) {
log.warn(`Logo width must be a multiple of 2. It has been rounded to ${logoWidth}dp.`);
}
if (brandWidth < args.brandWidth) {
log.warn(`Brand width must be a multiple of 2. It has been rounded to ${brandWidth}dp.`);
}
const fileNameSuffix = await getFileNameSuffix({
background,
brand,
brandWidth,
darkBackground,
darkBrand,
darkLogo,
logo,
logoWidth
});
const androidOutputPath = getAndroidOutputPath({
assetsOutputPath,
brandHeight,
brandWidth,
flavor,
isExpo,
logoHeight,
logoWidth,
platforms
});
const iosOutputPath = getIOSOutputPath({
assetsOutputPath,
isExpo,
platforms
});
const htmlTemplatePath = await getHtmlTemplatePath({
isExpo,
html,
platforms
});
if (androidOutputPath != null) {
log.title("🤖", "Android");
hfs.ensureDir(androidOutputPath);
await Promise.all([{
ratio: 1,
suffix: "mdpi"
}, {
ratio: 1.5,
suffix: "hdpi"
}, {
ratio: 2,
suffix: "xhdpi"
}, {
ratio: 3,
suffix: "xxhdpi"
}, {
ratio: 4,
suffix: "xxxhdpi"
}].map(({
ratio,
suffix
}) => {
const drawableDirPath = _path.default.resolve(androidOutputPath, `drawable-${suffix}`);
hfs.ensureDir(drawableDirPath);
// https://developer.android.com/develop/ui/views/launch/splash-screen#dimensions
const canvasSize = 288 * ratio;
// https://sharp.pixelplumbing.com/api-constructor
const canvas = (0, _sharp.default)({
create: {
width: canvasSize,
height: canvasSize,
channels: 4,
background: {
r: 255,
g: 255,
b: 255,
alpha: 0
}
}
});
const filePath = _path.default.resolve(drawableDirPath, "bootsplash_logo.png");
return logo.clone().resize(logoWidth * ratio).toBuffer().then(input => canvas.composite([{
input
}]).png({
quality: 100
}).toFile(filePath)).then(() => {
log.write(filePath, {
width: canvasSize,
height: canvasSize
});
});
}));
if (!isExpo) {
const manifestXmlPath = _path.default.resolve(androidOutputPath, "..", "AndroidManifest.xml");
if (hfs.exists(manifestXmlPath)) {
const manifestXml = readXmlLike(manifestXmlPath);
const activities = manifestXml.root.querySelectorAll("activity");
for (const activity of activities) {
if (activity.getAttribute("android:name") === ".MainActivity") {
activity.setAttribute("android:theme", "@style/BootTheme");
}
}
await writeXmlLike(manifestXmlPath, manifestXml.root.toString(), {
...manifestXml.formatOptions,
formatter: "prettier",
htmlWhitespaceSensitivity: "ignore",
selfClosingTags: true,
singleAttributePerLine: true
});
} else {
log.warn("No AndroidManifest.xml found. Skipping…");
}
const valuesPath = _path.default.resolve(androidOutputPath, "values");
hfs.ensureDir(valuesPath);
const colorsXmlPath = _path.default.resolve(valuesPath, "colors.xml");
const colorsXmlEntry = `<color name="bootsplash_background">${background.hex}</color>`;
if (hfs.exists(colorsXmlPath)) {
const colorsXml = readXmlLike(colorsXmlPath);
const nextColor = (0, _nodeHtmlParser.parse)(colorsXmlEntry);
const prevColor = colorsXml.root.querySelector('color[name="bootsplash_background"]');
if (prevColor != null) {
prevColor.replaceWith(nextColor);
} else {
colorsXml.root.querySelector("resources")?.appendChild(nextColor);
}
await writeXmlLike(colorsXmlPath, colorsXml.root.toString(), {
...colorsXml.formatOptions,
formatter: "xmlFormatter"
});
} else {
await writeXmlLike(colorsXmlPath, `<resources>${colorsXmlEntry}</resources>`, {
formatter: "xmlFormatter"
});
}
const stylesXmlPath = _path.default.resolve(valuesPath, "styles.xml");
if (hfs.exists(stylesXmlPath)) {
const stylesXml = readXmlLike(stylesXmlPath);
const prevStyle = stylesXml.root.querySelector('style[name="BootTheme"]');
const parent = prevStyle?.getAttribute("parent") ?? "Theme.BootSplash";
const extraItems = (0, _nodeHtmlParser.parse)(prevStyle?.text.split("\n").map(line => line.trim()).join("") ?? "").childNodes.filter(node => {
if (!(node instanceof _nodeHtmlParser.HTMLElement)) {
return true;
}
const name = node.getAttribute("name");
return name !== "bootSplashBackground" && name !== "bootSplashLogo" && name !== "bootSplashBrand" && name !== "postBootSplashTheme";
}).map(node => node.toString());
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>'];
const nextStyle = (0, _nodeHtmlParser.parse)((0, _tsDedent.dedent)`
<style name="BootTheme" parent="${parent}">
${styleItems.join("\n")}
</style>
`);
prevStyle?.remove(); // remove the existing style
stylesXml.root.querySelector("resources")?.appendChild(nextStyle);
await writeXmlLike(stylesXmlPath, stylesXml.root.toString(), {
...stylesXml.formatOptions,
formatter: "prettier",
htmlWhitespaceSensitivity: "ignore"
});
} else {
log.warn("No styles.xml found. Skipping…");
}
}
}
if (iosOutputPath != null) {
log.title("🍏", "iOS");
hfs.ensureDir(iosOutputPath);
cleanIOSAssets(iosOutputPath);
const storyboardPath = _path.default.resolve(iosOutputPath, "BootSplash.storyboard");
const colorsSetPath = _path.default.resolve(iosOutputPath, "Colors.xcassets", `BootSplashBackground-${fileNameSuffix}.colorset`);
const imageSetPath = _path.default.resolve(iosOutputPath, "Images.xcassets", `BootSplashLogo-${fileNameSuffix}.imageset`);
hfs.ensureDir(colorsSetPath);
hfs.ensureDir(imageSetPath);
await writeXmlLike(storyboardPath, getStoryboard({
logoHeight,
logoWidth,
background: background.rgb,
fileNameSuffix
}), {
formatter: "xmlFormatter",
whiteSpaceAtEndOfSelfclosingTag: false
});
writeJson(_path.default.resolve(colorsSetPath, "Contents.json"), {
colors: [{
idiom: "universal",
color: {
"color-space": "srgb",
components: {
blue: background.rgb.B,
green: background.rgb.G,
red: background.rgb.R,
alpha: "1.000"
}
}
}],
info: {
author: "xcode",
version: 1
}
});
const logoFileName = `logo-${fileNameSuffix}`;
writeJson(_path.default.resolve(imageSetPath, "Contents.json"), {
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: {
author: "xcode",
version: 1
}
});
await Promise.all([{
ratio: 1,
suffix: ""
}, {
ratio: 2,
suffix: "@2x"
}, {
ratio: 3,
suffix: "@3x"
}].map(({
ratio,
suffix
}) => {
const filePath = _path.default.resolve(imageSetPath, `${logoFileName}${suffix}.png`);
return logo.clone().resize(logoWidth * ratio).png({
quality: 100
}).toFile(filePath).then(({
width,
height
}) => {
log.write(filePath, {
width,
height
});
});
}));
if (!isExpo) {
const infoPlistPath = _path.default.resolve(iosOutputPath, "Info.plist");
const infoPlist = _plist.default.parse(hfs.text(infoPlistPath));
infoPlist["UILaunchStoryboardName"] = "BootSplash";
const formatted = (0, _xmlFormatter.default)(_plist.default.build(infoPlist), {
collapseContent: true,
forceSelfClosingEmptyTag: false,
indentation: "\t",
lineSeparator: "\n",
whiteSpaceAtEndOfSelfclosingTag: false
}).replace(/<string\/>/gm, "<string></string>").replace(/^\t/gm, "");
hfs.write(infoPlistPath, formatted);
log.write(infoPlistPath);
const pbxprojectPath = Expo.IOSConfig.Paths.getPBXProjectPath(projectRoot);
const xcodeProjectPath = Expo.IOSConfig.Paths.getXcodeProjectPath(projectRoot);
const project = Expo.IOSConfig.XcodeUtils.getPbxproj(projectRoot);
const projectName = _path.default.basename(iosOutputPath);
const groupName = _path.default.parse(xcodeProjectPath).name;
Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
project,
filepath: _path.default.join(projectName, "BootSplash.storyboard"),
groupName,
isBuildFile: true
});
Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
project,
filepath: _path.default.join(projectName, "Colors.xcassets"),
groupName,
isBuildFile: true
});
hfs.write(pbxprojectPath, project.writeSync());
log.write(pbxprojectPath);
}
}
if (htmlTemplatePath != null) {
log.title("🌐", "Web");
const htmlTemplate = readXmlLike(htmlTemplatePath);
const {
format
} = await logo.metadata();
const prevStyle = htmlTemplate.root.querySelector("#bootsplash-style");
const base64 = (format === "svg" ? hfs.buffer(logoPath) : await logo.clone().resize(Math.round(logoWidth * 2)).png({
quality: 100
}).toBuffer()).toString("base64");
const dataURI = `data:image/${format ? "svg+xml" : "png"};base64,${base64}`;
const nextStyle = (0, _nodeHtmlParser.parse)((0, _tsDedent.dedent)`
<style id="bootsplash-style">
#bootsplash {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
background-color: ${background.hex};
}
#bootsplash-logo {
content: url("${dataURI}");
width: ${logoWidth}px;
height: ${logoHeight}px;
}
</style>
`);
if (prevStyle != null) {
prevStyle.replaceWith(nextStyle);
} else {
htmlTemplate.root.querySelector("head")?.appendChild(nextStyle);
}
const prevDiv = htmlTemplate.root.querySelector("#bootsplash");
const nextDiv = (0, _nodeHtmlParser.parse)((0, _tsDedent.dedent)`
<div id="bootsplash">
<div id="bootsplash-logo"></div>
</div>
`);
if (prevDiv != null) {
prevDiv.replaceWith(nextDiv);
} else {
htmlTemplate.root.querySelector("body")?.appendChild(nextDiv);
}
await writeXmlLike(htmlTemplatePath, htmlTemplate.root.toString(), {
...htmlTemplate.formatOptions,
formatter: "prettier",
useCssPlugin: true
});
}
log.title("📄", "Assets");
hfs.ensureDir(assetsOutputPath);
writeJson(_path.default.resolve(assetsOutputPath, "manifest.json"), {
background: background.hex,
logo: {
width: logoWidth,
height: logoHeight
}
});
await Promise.all([{
ratio: 1,
suffix: ""
}, {
ratio: 1.5,
suffix: "@1,5x"
}, {
ratio: 2,
suffix: "@2x"
}, {
ratio: 3,
suffix: "@3x"
}, {
ratio: 4,
suffix: "@4x"
}].map(({
ratio,
suffix
}) => {
const filePath = _path.default.resolve(assetsOutputPath, `logo${suffix}.png`);
return logo.clone().resize(Math.round(logoWidth * ratio)).png({
quality: 100
}).toFile(filePath).then(({
width,
height
}) => {
log.write(filePath, {
width,
height
});
});
}));
if (licenseKey != null && executeAddon) {
const addon = requireAddon();
await addon?.execute({
licenseKey,
isExpo,
fileNameSuffix,
androidOutputPath,
iosOutputPath,
htmlTemplatePath,
assetsOutputPath,
logoHeight,
logoWidth,
brandHeight,
brandWidth,
logoPath,
darkLogoPath,
brandPath,
darkBrandPath,
background,
logo,
brand,
darkBackground,
darkLogo,
darkBrand
});
} else {
console.log(`
${_picocolors.default.blue("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")}
${_picocolors.default.blue("┃")} 🔑 ${_picocolors.default.bold("Get a license key for brand image / dark mode support")} ${_picocolors.default.blue("┃")}
${_picocolors.default.blue("┃")} ${_picocolors.default.underline("https://zoontek.gumroad.com/l/bootsplash-generator")} ${_picocolors.default.blue("┃")}
${_picocolors.default.blue("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")}`);
}
console.log(`\n💖 Thanks for using ${_picocolors.default.underline("react-native-bootsplash")}`);
};
exports.generate = generate;
//# sourceMappingURL=generate.js.map