react-native-bootsplash
Version:
Display a bootsplash on your app starts. Hide it when you want.
387 lines (325 loc) • 10.6 kB
text/typescript
import * as Expo from "@expo/config-plugins";
import { assignColorValue } from "@expo/config-plugins/build/android/Colors";
import { addImports } from "@expo/config-plugins/build/android/codeMod";
import { mergeContents } from "@expo/config-plugins/build/utils/generateCode";
import childProcess from "child_process";
import path from "path";
import semver from "semver";
import { dedent } from "ts-dedent";
import util from "util";
import {
type BootSplashPluginConfig,
hfs,
log,
PACKAGE_NAME,
requireAddon,
setLoggerMode,
transformProps,
writeAndroidAssets,
writeGenericAssets,
writeIOSAssets,
writeWebAssets,
} from "./utils";
const promisifiedExec = util.promisify(childProcess.exec);
const exec = (cmd: string) =>
promisifiedExec(cmd).then(({ stdout, stderr }) => stdout || stderr);
type ConfigPlugin = Expo.ConfigPlugin<BootSplashPluginConfig>;
const withoutExpoSplashScreen: ConfigPlugin = Expo.createRunOncePlugin(
(expoConfig) => expoConfig,
"expo-splash-screen",
"skip",
);
const withAndroidAssets: ConfigPlugin = (expoConfig, rawProps) =>
Expo.withDangerousMod(expoConfig, [
"android",
async (config) => {
const { platformProjectRoot, projectRoot } = config.modRequest;
const props = await transformProps(projectRoot, rawProps);
const addon = requireAddon(props);
const androidOutputPath = path.resolve(
platformProjectRoot,
"app",
"src",
"main",
"res",
);
await writeAndroidAssets({ androidOutputPath, props });
await addon?.writeAndroidAssets({ androidOutputPath, props });
return config;
},
]);
const withAndroidManifest: ConfigPlugin = (expoConfig) =>
Expo.withAndroidManifest(expoConfig, (config) => {
config.modResults.manifest.application?.forEach((application) => {
if (application.$["android:name"] === ".MainApplication") {
const { activity } = application;
activity?.forEach((activity) => {
if (activity.$["android:name"] === ".MainActivity") {
activity.$["android:theme"] = "@style/BootTheme";
}
});
}
});
return config;
});
const withMainActivity: ConfigPlugin = (expoConfig) =>
Expo.withMainActivity(expoConfig, (config) => {
const { modResults } = config;
const withImports = addImports(
modResults.contents.replace(
/(\/\/ )?setTheme\(R\.style\.AppTheme\)/,
"// setTheme(R.style.AppTheme)",
),
["android.os.Bundle", "com.zoontek.rnbootsplash.RNBootSplash"],
false,
);
// indented with 4 spaces
const withInit = mergeContents({
src: withImports,
comment: " //",
tag: "bootsplash-init",
offset: 0,
anchor: /super\.onCreate\((null|savedInstanceState)\)/,
newSrc: " RNBootSplash.init(this, R.style.BootTheme)",
});
return {
...config,
modResults: {
...modResults,
contents: withInit.contents,
},
};
});
const withAndroidStyles: ConfigPlugin = (expoConfig, rawProps) =>
Expo.withAndroidStyles(expoConfig, async (config) => {
const { projectRoot } = config.modRequest;
const { android, brand } = await transformProps(projectRoot, rawProps);
const { darkContentBarsStyle } = android;
const { modResults } = config;
const { resources } = modResults;
const { style = [] } = resources;
const item = [
{
$: { name: "postBootSplashTheme" },
_: "@style/AppTheme",
},
{
$: { name: "bootSplashBackground" },
_: "@color/bootsplash_background",
},
{
$: { name: "bootSplashLogo" },
_: "@drawable/bootsplash_logo",
},
];
if (brand != null) {
item.push({
$: { name: "bootSplashBrand" },
_: "@drawable/bootsplash_brand",
});
}
if (darkContentBarsStyle != null) {
item.push({
$: { name: "darkContentBarsStyle" },
_: String(darkContentBarsStyle),
});
}
const withBootTheme = [
...style.filter(({ $ }) => $.name !== "BootTheme"),
{
$: {
name: "BootTheme",
parent: "Theme.BootSplash",
},
item,
},
];
return {
...config,
modResults: {
...modResults,
resources: {
...resources,
style: withBootTheme,
},
},
};
});
const withAndroidColors: ConfigPlugin = (expoConfig, rawProps) =>
Expo.withAndroidColors(expoConfig, async (config) => {
const { projectRoot } = config.modRequest;
const { background } = await transformProps(projectRoot, rawProps);
config.modResults = assignColorValue(config.modResults, {
name: "bootsplash_background",
value: background.hex,
});
return config;
});
const withAndroidColorsNight: ConfigPlugin = (expoConfig, rawProps) =>
Expo.withAndroidColorsNight(expoConfig, async (config) => {
const { projectRoot } = config.modRequest;
const props = await transformProps(projectRoot, rawProps);
const addon = requireAddon(props);
return addon != null
? await addon.withAndroidColorsNight({ config, props })
: config;
});
const withIOSAssets: ConfigPlugin = (expoConfig, rawProps) =>
Expo.withDangerousMod(expoConfig, [
"ios",
async (config) => {
const {
platformProjectRoot,
projectName = "",
projectRoot,
} = config.modRequest;
const props = await transformProps(projectRoot, rawProps);
const addon = requireAddon(props);
const iosOutputPath = path.resolve(platformProjectRoot, projectName);
await writeIOSAssets({ iosOutputPath, props });
await addon?.writeIOSAssets({ iosOutputPath, props });
return config;
},
]);
const withAppDelegate: ConfigPlugin = (expoConfig) =>
Expo.withAppDelegate(expoConfig, (config) => {
const { modResults } = config;
const { language } = modResults;
if (language !== "swift") {
throw new Error(
`Cannot modify the project AppDelegate as it's not in a supported language: ${language}`,
);
}
const withHeader = mergeContents({
src: modResults.contents,
comment: "//",
tag: "bootsplash-header",
offset: 1,
anchor: /import Expo/,
newSrc: "import RNBootSplash",
});
const withRootView = mergeContents({
src: withHeader.contents,
comment: "//",
tag: "bootsplash-init",
offset: 1,
anchor: /class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {/,
newSrc: dedent`
public override func customize(_ rootView: UIView) {
super.customize(rootView)
RNBootSplash.initWithStoryboard("BootSplash", rootView: rootView)
}
`,
});
return {
...config,
modResults: {
...modResults,
contents: withRootView.contents,
},
};
});
const withInfoPlist: ConfigPlugin = (expoConfig) =>
Expo.withInfoPlist(expoConfig, (config) => {
config.modResults["UILaunchStoryboardName"] = "BootSplash";
return config;
});
const withXcodeProject: ConfigPlugin = (expoConfig) =>
Expo.withXcodeProject(expoConfig, (config) => {
const { projectName = "" } = config.modRequest;
Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
filepath: path.join(projectName, "BootSplash.storyboard"),
groupName: projectName,
project: config.modResults,
isBuildFile: true,
});
Expo.IOSConfig.XcodeUtils.addResourceFileToGroup({
filepath: path.join(projectName, "Colors.xcassets"),
groupName: projectName,
project: config.modResults,
isBuildFile: true,
});
return config;
});
const withWebAssets: ConfigPlugin = (expoConfig, rawProps) =>
Expo.withDangerousMod(expoConfig, [
expoConfig.platforms?.includes("ios") ? "ios" : "android",
async (config) => {
const { projectRoot } = config.modRequest;
const props = await transformProps(projectRoot, rawProps);
const addon = requireAddon(props);
const fileName = "public/index.html";
const htmlTemplatePath = path.resolve(projectRoot, fileName);
if (!hfs.exists(htmlTemplatePath)) {
await exec(`npx expo customize ${fileName}`);
}
await writeWebAssets({ htmlTemplatePath, props });
await addon?.writeWebAssets({ htmlTemplatePath, props });
return config;
},
]);
const withGenericAssets: ConfigPlugin = (expoConfig, rawProps) =>
Expo.withDangerousMod(expoConfig, [
expoConfig.platforms?.includes("ios") ? "ios" : "android",
async (config) => {
const { projectRoot } = config.modRequest;
const props = await transformProps(projectRoot, rawProps);
const addon = requireAddon(props);
await writeGenericAssets({ props });
await addon?.writeGenericAssets({ props });
return config;
},
]);
export const withBootSplash = Expo.createRunOncePlugin<
(Partial<BootSplashPluginConfig> & { logo: string }) | undefined
>((expoConfig, rawProps) => {
const { platforms = [], sdkVersion = "0.1.0" } = expoConfig;
setLoggerMode({ type: "plugin" });
if (semver.lt(sdkVersion, "54.0.0")) {
log.error("Requires Expo 54.0.0 (or higher)");
process.exit(1);
}
if (!rawProps?.logo) {
log.error("Missing required parameter 'logo'");
process.exit(1);
}
const plugins: ConfigPlugin[] = [withoutExpoSplashScreen, withGenericAssets];
if (platforms.includes("android")) {
plugins.push(
withAndroidAssets,
withAndroidManifest,
withMainActivity,
withAndroidStyles,
withAndroidColors,
withAndroidColorsNight,
);
}
if (platforms.includes("ios")) {
plugins.push(
withIOSAssets,
withAppDelegate,
withInfoPlist,
withXcodeProject,
);
}
if (platforms.includes("web")) {
plugins.push(withWebAssets);
}
const licenseKey = process.env["BOOTSPLASH_LICENSE_KEY"];
if ("licenseKey" in rawProps) {
log.warn(
"Passing licenseKey directly in plugin config is deprecated as it could be leaked by expo-constants. Use the BOOTSPLASH_LICENSE_KEY environment variable instead.",
);
}
const rawPropsWithEnvVariable = {
...rawProps,
...(licenseKey != null && { licenseKey }),
};
return Expo.withPlugins(
expoConfig,
plugins.map((plugin) => [plugin, rawPropsWithEnvVariable]),
);
}, PACKAGE_NAME);
export default (
config: BootSplashPluginConfig,
): [typeof PACKAGE_NAME, BootSplashPluginConfig] => [PACKAGE_NAME, config];