UNPKG

react-native-bootsplash

Version:

Display a bootsplash on your app starts. Hide it when you want.

411 lines (337 loc) 11 kB
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"; 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 }: { flavor: string }) => { 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 }: { html: string }) => { 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, }: { iosOutputPath: string; plist: string | undefined; }) => { 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 }: { platforms: Array<"android" | "ios" | "web">; html: string; flavor: string; plist?: string; logo: string; background: string; logoWidth: number; assetsOutput: string; licenseKey?: string; brand?: string; brandWidth: number; darkBackground?: string; darkLogo?: string; darkBrand?: string; }) => { 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: string[] = [ ...(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)) as Record< string, unknown >; 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")}`, ); };