react-native-bootsplash
Version:
Display a bootsplash on your app starts. Hide it when you want.
267 lines (266 loc) • 10.1 kB
JavaScript
;
import * as Expo from "@expo/config-plugins";
import ExpoPlist from "@expo/plist";
import glob from "fast-glob";
import fs from "fs";
import { HTMLElement, parse as parseHtml } from "node-html-parser";
import path from "path";
import pc from "picocolors";
import { dedent } from "ts-dedent";
import formatXml from "xml-formatter";
import { hfs, log, readXmlLike, requireAddon, setLoggerMode, transformProps, writeAndroidAssets, writeGenericAssets, writeIOSAssets, writeWebAssets, writeXmlLike } from "./utils.js";
const cwd = process.env.INIT_CWD ?? process.env.PWD ?? process.cwd();
setLoggerMode({
type: "cli",
cwd
});
const projectRoot = (() => {
let directory = path.resolve(cwd);
const {
root
} = path.parse(directory);
const ends = new Set([root, path.resolve(directory, root)]);
while (directory) {
const pkgPath = path.join(directory, "package.json");
const stats = fs.statSync(pkgPath, {
throwIfNoEntry: false
});
if (stats != null && stats.isFile()) {
return path.dirname(pkgPath);
}
if (ends.has(directory)) {
break;
} else {
directory = path.dirname(directory);
}
}
})();
if (!projectRoot) {
log.error("We couldn't find a package.json in your project.");
process.exit(1);
}
const getAndroidOutputPath = ({
flavor
}) => {
const sourceDir = path.join(projectRoot, "android");
if (!fs.existsSync(sourceDir)) {
return;
}
const appDir = path.join(sourceDir, "app");
const androidOutputPath = path.resolve(sourceDir, fs.existsSync(appDir) ? "app" : "", "src", flavor, "res");
if (hfs.exists(androidOutputPath)) {
return androidOutputPath;
}
log.warn(`No ${path.relative(cwd, androidOutputPath)} directory found. Skipping Android assets generation…`);
};
const getIOSOutputPath = () => {
const podfile = glob.sync("**/Podfile", {
cwd: projectRoot.replace(/^([a-zA-Z]+:|\.\/)/, ""),
deep: 10,
ignore: ["**/@(Pods|node_modules|Carthage|vendor|android)/**"]
}).find(project => {
return path.dirname(project) === "ios";
});
if (!podfile) {
return;
}
const sourceDir = path.dirname(path.join(projectRoot, podfile));
const xcodeProjectName = fs.readdirSync(sourceDir).sort().reverse().find(fileName => {
const ext = path.extname(fileName);
return ext === ".xcodeproj" || ext === ".xcworkspace";
});
if (xcodeProjectName == null) {
log.warn("No Xcode project found. Skipping iOS assets generation…");
return;
}
const iosOutputPath = path.resolve(sourceDir, xcodeProjectName).replace(/\.(xcodeproj|xcworkspace)$/, "");
if (hfs.exists(iosOutputPath)) {
return iosOutputPath;
}
log.warn(`No ${path.relative(cwd, iosOutputPath)} directory found. Skipping iOS assets generation…`);
};
const getHtmlTemplatePath = ({
html
}) => {
const htmlTemplatePath = path.resolve(cwd, html);
if (hfs.exists(htmlTemplatePath)) {
return htmlTemplatePath;
}
log.warn(`No ${path.relative(cwd, htmlTemplatePath)} found. Skipping HTML + CSS generation…`);
};
const getInfoPlistPath = ({
iosOutputPath,
plist
}) => {
if (plist != null) {
const infoPlistPath = path.resolve(cwd, plist);
if (!hfs.exists(infoPlistPath)) {
return log.warn(`No ${path.relative(cwd, infoPlistPath)} found`);
}
return infoPlistPath;
}
return path.resolve(iosOutputPath, "Info.plist");
};
export const generate = async ({
platforms,
html,
flavor,
plist,
...rawProps
}) => {
const props = await transformProps(cwd, rawProps);
const addon = requireAddon(props);
const {
background,
brand
} = props;
const androidOutputPath = platforms.includes("android") ? getAndroidOutputPath({
flavor
}) : undefined;
const iosOutputPath = platforms.includes("ios") ? getIOSOutputPath() : undefined;
const htmlTemplatePath = platforms.includes("web") ? getHtmlTemplatePath({
html
}) : undefined;
if (androidOutputPath != null) {
await writeAndroidAssets({
androidOutputPath,
props
});
const manifestXmlPath = path.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");
}
const valuesPath = path.resolve(androidOutputPath, "values");
hfs.ensureDir(valuesPath);
const colorsXmlPath = path.resolve(valuesPath, "colors.xml");
const colorsXmlEntry = `<color name="bootsplash_background">${background.hex}</color>`;
if (hfs.exists(colorsXmlPath)) {
const colorsXml = readXmlLike(colorsXmlPath);
const nextColor = parseHtml(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.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 = parseHtml(prevStyle?.text.split("\n").map(line => line.trim()).join("") ?? "").childNodes.filter(node => {
if (!(node instanceof 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 ? ['<item name="bootSplashBrand">@drawable/bootsplash_brand</item>'] : []), '<item name="postBootSplashTheme">@style/AppTheme</item>'];
const nextStyle = parseHtml(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");
}
}
if (iosOutputPath != null) {
await writeIOSAssets({
iosOutputPath,
props
});
const pbxprojectPath = Expo.IOSConfig.Paths.getPBXProjectPath(projectRoot);
const xcodeProjectPath = Expo.IOSConfig.Paths.getXcodeProjectPath(projectRoot);
const project = Expo.IOSConfig.XcodeUtils.getPbxproj(projectRoot);
const projectName = path.basename(iosOutputPath);
const groupName = path.parse(xcodeProjectPath).name;
Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
project,
filepath: path.join(projectName, "BootSplash.storyboard"),
groupName,
isBuildFile: true
});
Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
project,
filepath: path.join(projectName, "Colors.xcassets"),
groupName,
isBuildFile: true
});
hfs.write(pbxprojectPath, project.writeSync());
log.write(pbxprojectPath);
const infoPlistPath = getInfoPlistPath({
iosOutputPath,
plist
});
if (infoPlistPath != null) {
const infoPlist = ExpoPlist.parse(hfs.text(infoPlistPath));
infoPlist["UILaunchStoryboardName"] = "BootSplash";
const formatted = formatXml(ExpoPlist.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);
}
}
if (htmlTemplatePath != null) {
await writeWebAssets({
htmlTemplatePath,
props
});
}
await writeGenericAssets({
props
});
if (addon != null) {
await addon.execute({
props,
androidOutputPath,
iosOutputPath,
htmlTemplatePath
});
} else {
console.log(`
${pc.blue("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓")}
${pc.blue("┃")} 🔑 ${pc.bold("Get a license key for brand image / dark mode support")} ${pc.blue("┃")}
${pc.blue("┃")} ${pc.underline("https://zoontek.gumroad.com/l/bootsplash-generator")} ${pc.blue("┃")}
${pc.blue("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")}`);
}
console.log(`\n💖 Thanks for using ${pc.underline("react-native-bootsplash")}`);
};
//# sourceMappingURL=generate.js.map