first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
export type ContentsJsonImageIdiom = 'iphone' | 'ipad' | 'watchos' | 'ios' | 'ios-marketing' | 'universal';
export type ContentsJsonImageAppearanceLuminosityType = 'light' | 'dark' | 'tinted';
export type ContentsJsonAppearance = {
appearance: 'luminosity';
value: ContentsJsonImageAppearanceLuminosityType;
};
export type ContentsJsonImageScale = '1x' | '2x' | '3x';
export interface ContentsJsonImage {
appearances?: ContentsJsonAppearance[];
idiom: ContentsJsonImageIdiom;
size?: string;
scale?: ContentsJsonImageScale;
filename?: string;
platform?: ContentsJsonImageIdiom;
}
export interface ContentsJsonColor {
appearances?: ContentsJsonAppearance[];
idiom: ContentsJsonImageIdiom;
color: {
'color-space': 'srgb';
components: {
alpha: string;
blue: string;
green: string;
red: string;
};
};
}
export interface ContentsJson {
images: ContentsJsonImage[];
colors: ContentsJsonColor[];
info: {
version: number;
author: string;
};
}
export declare function createContentsJsonItem(item: ContentsJsonImage): ContentsJsonImage;
/**
* Writes the Config.json which is used to assign images to their respective platform, dpi, and idiom.
*
* @param directory path to add the Contents.json to.
* @param contents image json data
*/
export declare function writeContentsJsonAsync(directory: string, { images }: Pick<ContentsJson, 'images'>): Promise<void>;

View File

@@ -0,0 +1,48 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createContentsJsonItem = createContentsJsonItem;
exports.writeContentsJsonAsync = writeContentsJsonAsync;
function _fs() {
const data = _interopRequireDefault(require("fs"));
_fs = function () {
return data;
};
return data;
}
function _path() {
const data = require("path");
_path = function () {
return data;
};
return data;
}
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function createContentsJsonItem(item) {
return item;
}
/**
* Writes the Config.json which is used to assign images to their respective platform, dpi, and idiom.
*
* @param directory path to add the Contents.json to.
* @param contents image json data
*/
async function writeContentsJsonAsync(directory, {
images
}) {
await _fs().default.promises.mkdir(directory, {
recursive: true
});
await _fs().default.promises.writeFile((0, _path().join)(directory, 'Contents.json'), JSON.stringify({
images,
info: {
version: 1,
// common practice is for the tool that generated the icons to be the "author"
author: 'expo'
}
}, null, 2));
}
//# sourceMappingURL=AssetContents.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"AssetContents.js","names":["_fs","data","_interopRequireDefault","require","_path","e","__esModule","default","createContentsJsonItem","item","writeContentsJsonAsync","directory","images","fs","promises","mkdir","recursive","writeFile","join","JSON","stringify","info","version","author"],"sources":["../../../src/plugins/icons/AssetContents.ts"],"sourcesContent":["import fs from 'fs';\nimport { join } from 'path';\n\nexport type ContentsJsonImageIdiom =\n | 'iphone'\n | 'ipad'\n | 'watchos'\n | 'ios'\n | 'ios-marketing'\n | 'universal';\n\nexport type ContentsJsonImageAppearanceLuminosityType = 'light' | 'dark' | 'tinted';\n\nexport type ContentsJsonAppearance = {\n appearance: 'luminosity';\n value: ContentsJsonImageAppearanceLuminosityType;\n};\n\nexport type ContentsJsonImageScale = '1x' | '2x' | '3x';\n\nexport interface ContentsJsonImage {\n appearances?: ContentsJsonAppearance[];\n idiom: ContentsJsonImageIdiom;\n size?: string;\n scale?: ContentsJsonImageScale;\n filename?: string;\n platform?: ContentsJsonImageIdiom;\n}\n\nexport interface ContentsJsonColor {\n appearances?: ContentsJsonAppearance[];\n idiom: ContentsJsonImageIdiom;\n color: {\n 'color-space': 'srgb';\n components: {\n alpha: string;\n blue: string;\n green: string;\n red: string;\n };\n };\n}\n\nexport interface ContentsJson {\n images: ContentsJsonImage[];\n colors: ContentsJsonColor[];\n info: {\n version: number;\n author: string;\n };\n}\n\nexport function createContentsJsonItem(item: ContentsJsonImage): ContentsJsonImage {\n return item;\n}\n\n/**\n * Writes the Config.json which is used to assign images to their respective platform, dpi, and idiom.\n *\n * @param directory path to add the Contents.json to.\n * @param contents image json data\n */\nexport async function writeContentsJsonAsync(\n directory: string,\n { images }: Pick<ContentsJson, 'images'>\n): Promise<void> {\n await fs.promises.mkdir(directory, { recursive: true });\n await fs.promises.writeFile(\n join(directory, 'Contents.json'),\n JSON.stringify(\n {\n images,\n info: {\n version: 1,\n // common practice is for the tool that generated the icons to be the \"author\"\n author: 'expo',\n },\n },\n null,\n 2\n )\n );\n}\n"],"mappings":";;;;;;;AAAA,SAAAA,IAAA;EAAA,MAAAC,IAAA,GAAAC,sBAAA,CAAAC,OAAA;EAAAH,GAAA,YAAAA,CAAA;IAAA,OAAAC,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AACA,SAAAG,MAAA;EAAA,MAAAH,IAAA,GAAAE,OAAA;EAAAC,KAAA,YAAAA,CAAA;IAAA,OAAAH,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AAA4B,SAAAC,uBAAAG,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAmDrB,SAASG,sBAAsBA,CAACC,IAAuB,EAAqB;EACjF,OAAOA,IAAI;AACb;;AAEA;AACA;AACA;AACA;AACA;AACA;AACO,eAAeC,sBAAsBA,CAC1CC,SAAiB,EACjB;EAAEC;AAAqC,CAAC,EACzB;EACf,MAAMC,aAAE,CAACC,QAAQ,CAACC,KAAK,CAACJ,SAAS,EAAE;IAAEK,SAAS,EAAE;EAAK,CAAC,CAAC;EACvD,MAAMH,aAAE,CAACC,QAAQ,CAACG,SAAS,CACzB,IAAAC,YAAI,EAACP,SAAS,EAAE,eAAe,CAAC,EAChCQ,IAAI,CAACC,SAAS,CACZ;IACER,MAAM;IACNS,IAAI,EAAE;MACJC,OAAO,EAAE,CAAC;MACV;MACAC,MAAM,EAAE;IACV;EACF,CAAC,EACD,IAAI,EACJ,CACF,CACF,CAAC;AACH","ignoreList":[]}

View File

@@ -0,0 +1,39 @@
import { AndroidConfig, ConfigPlugin } from '@expo/config-plugins';
import { ExpoConfig } from '@expo/config-types';
type DPIString = 'mdpi' | 'hdpi' | 'xhdpi' | 'xxhdpi' | 'xxxhdpi';
type dpiMap = Record<DPIString, {
folderName: string;
scale: number;
}>;
export declare const dpiValues: dpiMap;
export declare const ANDROID_RES_PATH = "android/app/src/main/res/";
export declare const withAndroidIcons: ConfigPlugin;
export declare function setRoundIconManifest(config: Pick<ExpoConfig, 'android'>, manifest: AndroidConfig.Manifest.AndroidManifest): AndroidConfig.Manifest.AndroidManifest;
export declare function getIcon(config: ExpoConfig): string | null;
export declare function getAdaptiveIcon(config: ExpoConfig): {
foregroundImage: string | null;
backgroundColor: string | null;
backgroundImage: string | null;
monochromeImage: string | null;
};
/**
* Resizes the user-provided icon to create a set of legacy icon files in
* their respective "mipmap" directories for <= Android 7, and creates a set of adaptive
* icon files for > Android 7 from the adaptive icon files (if provided).
*/
export declare function setIconAsync(projectRoot: string, { icon, backgroundColor, backgroundImage, monochromeImage, isAdaptive, }: {
icon: string | null;
backgroundColor: string | null;
backgroundImage: string | null;
monochromeImage: string | null;
isAdaptive: boolean;
}): Promise<true | null>;
/**
* Configures adaptive icon files to be used on Android 8 and up. A foreground image must be provided,
* and will have a transparent background unless:
* - A backgroundImage is provided, or
* - A backgroundColor was specified
*/
export declare function configureAdaptiveIconAsync(projectRoot: string, foregroundImage: string, backgroundImage: string | null, monochromeImage: string | null, isAdaptive: boolean): Promise<void>;
export declare const createAdaptiveIconXmlString: (backgroundImage: string | null, monochromeImage: string | null) => string;
export {};

View File

@@ -0,0 +1,372 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.ANDROID_RES_PATH = void 0;
exports.configureAdaptiveIconAsync = configureAdaptiveIconAsync;
exports.dpiValues = exports.createAdaptiveIconXmlString = void 0;
exports.getAdaptiveIcon = getAdaptiveIcon;
exports.getIcon = getIcon;
exports.setIconAsync = setIconAsync;
exports.setRoundIconManifest = setRoundIconManifest;
exports.withAndroidIcons = void 0;
function _configPlugins() {
const data = require("@expo/config-plugins");
_configPlugins = function () {
return data;
};
return data;
}
function _imageUtils() {
const data = require("@expo/image-utils");
_imageUtils = function () {
return data;
};
return data;
}
function _fs() {
const data = _interopRequireDefault(require("fs"));
_fs = function () {
return data;
};
return data;
}
function _path() {
const data = _interopRequireDefault(require("path"));
_path = function () {
return data;
};
return data;
}
function _withAndroidManifestIcons() {
const data = require("./withAndroidManifestIcons");
_withAndroidManifestIcons = function () {
return data;
};
return data;
}
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const {
Colors
} = _configPlugins().AndroidConfig;
const dpiValues = exports.dpiValues = {
mdpi: {
folderName: 'mipmap-mdpi',
scale: 1
},
hdpi: {
folderName: 'mipmap-hdpi',
scale: 1.5
},
xhdpi: {
folderName: 'mipmap-xhdpi',
scale: 2
},
xxhdpi: {
folderName: 'mipmap-xxhdpi',
scale: 3
},
xxxhdpi: {
folderName: 'mipmap-xxxhdpi',
scale: 4
}
};
const LEGACY_BASELINE_PIXEL_SIZE = 48;
const ADAPTIVE_BASELINE_PIXEL_SIZE = 108;
const ANDROID_RES_PATH = exports.ANDROID_RES_PATH = 'android/app/src/main/res/';
const MIPMAP_ANYDPI_V26 = 'mipmap-anydpi-v26';
const ICON_BACKGROUND = 'iconBackground';
const IC_LAUNCHER_WEBP = 'ic_launcher.webp';
const IC_LAUNCHER_ROUND_WEBP = 'ic_launcher_round.webp';
const IC_LAUNCHER_BACKGROUND_WEBP = 'ic_launcher_background.webp';
const IC_LAUNCHER_FOREGROUND_WEBP = 'ic_launcher_foreground.webp';
const IC_LAUNCHER_MONOCHROME_WEBP = 'ic_launcher_monochrome.webp';
const IC_LAUNCHER_XML = 'ic_launcher.xml';
const IC_LAUNCHER_ROUND_XML = 'ic_launcher_round.xml';
const withAndroidIcons = config => {
const {
foregroundImage,
backgroundColor,
backgroundImage,
monochromeImage
} = getAdaptiveIcon(config);
const icon = foregroundImage ?? getIcon(config);
if (!icon) {
return config;
}
config = (0, _withAndroidManifestIcons().withAndroidManifestIcons)(config);
// Apply colors.xml changes
config = withAndroidAdaptiveIconColors(config, backgroundColor);
return (0, _configPlugins().withDangerousMod)(config, ['android', async config => {
await setIconAsync(config.modRequest.projectRoot, {
icon,
backgroundColor,
backgroundImage,
monochromeImage,
isAdaptive: !!config.android?.adaptiveIcon
});
return config;
}]);
};
exports.withAndroidIcons = withAndroidIcons;
function setRoundIconManifest(config, manifest) {
const isAdaptive = !!config.android?.adaptiveIcon;
const application = _configPlugins().AndroidConfig.Manifest.getMainApplicationOrThrow(manifest);
if (isAdaptive) {
application.$['android:roundIcon'] = '@mipmap/ic_launcher_round';
} else {
delete application.$['android:roundIcon'];
}
return manifest;
}
const withAndroidAdaptiveIconColors = (config, backgroundColor) => {
return (0, _configPlugins().withAndroidColors)(config, config => {
config.modResults = setBackgroundColor(backgroundColor ?? '#ffffff', config.modResults);
return config;
});
};
function getIcon(config) {
return config.android?.icon || config.icon || null;
}
function getAdaptiveIcon(config) {
return {
foregroundImage: config.android?.adaptiveIcon?.foregroundImage ?? null,
backgroundColor: config.android?.adaptiveIcon?.backgroundColor ?? null,
backgroundImage: config.android?.adaptiveIcon?.backgroundImage ?? null,
monochromeImage: config.android?.adaptiveIcon?.monochromeImage ?? null
};
}
/**
* Resizes the user-provided icon to create a set of legacy icon files in
* their respective "mipmap" directories for <= Android 7, and creates a set of adaptive
* icon files for > Android 7 from the adaptive icon files (if provided).
*/
async function setIconAsync(projectRoot, {
icon,
backgroundColor,
backgroundImage,
monochromeImage,
isAdaptive
}) {
if (!icon) {
return null;
}
await configureLegacyIconAsync(projectRoot, icon, backgroundImage, backgroundColor);
if (isAdaptive) {
await generateRoundIconAsync(projectRoot, icon, backgroundImage, backgroundColor);
} else {
await deleteIconNamedAsync(projectRoot, IC_LAUNCHER_ROUND_WEBP);
}
await configureAdaptiveIconAsync(projectRoot, icon, backgroundImage, monochromeImage, isAdaptive);
return true;
}
/**
* Configures legacy icon files to be used on Android 7 and earlier. If adaptive icon configuration
* was provided, we create a pseudo-adaptive icon by layering the provided files (or background
* color if no backgroundImage is provided. If no backgroundImage and no backgroundColor are provided,
* the background is set to transparent.)
*/
async function configureLegacyIconAsync(projectRoot, icon, backgroundImage, backgroundColor) {
return generateMultiLayerImageAsync(projectRoot, {
icon,
backgroundImage,
backgroundColor,
outputImageFileName: IC_LAUNCHER_WEBP,
imageCacheFolder: 'android-standard-square',
backgroundImageCacheFolder: 'android-standard-square-background'
});
}
async function generateRoundIconAsync(projectRoot, icon, backgroundImage, backgroundColor) {
return generateMultiLayerImageAsync(projectRoot, {
icon,
borderRadiusRatio: 0.5,
outputImageFileName: IC_LAUNCHER_ROUND_WEBP,
backgroundImage,
backgroundColor,
imageCacheFolder: 'android-standard-circle',
backgroundImageCacheFolder: 'android-standard-round-background',
isAdaptive: false
});
}
/**
* Configures adaptive icon files to be used on Android 8 and up. A foreground image must be provided,
* and will have a transparent background unless:
* - A backgroundImage is provided, or
* - A backgroundColor was specified
*/
async function configureAdaptiveIconAsync(projectRoot, foregroundImage, backgroundImage, monochromeImage, isAdaptive) {
if (monochromeImage) {
await generateMonochromeImageAsync(projectRoot, {
icon: monochromeImage,
imageCacheFolder: 'android-adaptive-monochrome',
outputImageFileName: IC_LAUNCHER_MONOCHROME_WEBP
});
}
await generateMultiLayerImageAsync(projectRoot, {
backgroundColor: 'transparent',
backgroundImage,
backgroundImageCacheFolder: 'android-adaptive-background',
outputImageFileName: IC_LAUNCHER_FOREGROUND_WEBP,
icon: foregroundImage,
imageCacheFolder: 'android-adaptive-foreground',
backgroundImageFileName: IC_LAUNCHER_BACKGROUND_WEBP,
isAdaptive: true
});
// create ic_launcher.xml and ic_launcher_round.xml
const icLauncherXmlString = createAdaptiveIconXmlString(backgroundImage, monochromeImage);
await createAdaptiveIconXmlFiles(projectRoot, icLauncherXmlString,
// If the user only defined icon and not android.adaptiveIcon, then skip enabling the layering system
// this will scale the image down and present it uncropped.
isAdaptive);
}
function setBackgroundColor(backgroundColor, colors) {
return Colors.assignColorValue(colors, {
value: backgroundColor,
name: ICON_BACKGROUND
});
}
const createAdaptiveIconXmlString = (backgroundImage, monochromeImage) => {
const background = backgroundImage ? `@mipmap/ic_launcher_background` : `@color/iconBackground`;
const iconElements = [`<background android:drawable="${background}"/>`, '<foreground android:drawable="@mipmap/ic_launcher_foreground"/>'];
if (monochromeImage) {
iconElements.push('<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>');
}
return `<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
${iconElements.join('\n ')}
</adaptive-icon>`;
};
exports.createAdaptiveIconXmlString = createAdaptiveIconXmlString;
async function createAdaptiveIconXmlFiles(projectRoot, icLauncherXmlString, add) {
const anyDpiV26Directory = _path().default.resolve(projectRoot, ANDROID_RES_PATH, MIPMAP_ANYDPI_V26);
await _fs().default.promises.mkdir(anyDpiV26Directory, {
recursive: true
});
const launcherPath = _path().default.resolve(anyDpiV26Directory, IC_LAUNCHER_XML);
const launcherRoundPath = _path().default.resolve(anyDpiV26Directory, IC_LAUNCHER_ROUND_XML);
if (add) {
await Promise.all([_fs().default.promises.writeFile(launcherPath, icLauncherXmlString, 'utf8'), _fs().default.promises.writeFile(launcherRoundPath, icLauncherXmlString, 'utf8')]);
} else {
// Remove the xml if the icon switches from adaptive to standard.
await Promise.all([launcherPath, launcherRoundPath].map(async path => {
return _fs().default.promises.rm(path, {
force: true
});
}));
}
}
async function generateMultiLayerImageAsync(projectRoot, {
icon,
backgroundColor,
backgroundImage,
imageCacheFolder,
backgroundImageCacheFolder,
borderRadiusRatio,
outputImageFileName,
backgroundImageFileName,
isAdaptive
}) {
await iterateDpiValues(projectRoot, async ({
dpiFolder,
scale
}) => {
let iconLayer = await generateIconAsync(projectRoot, {
cacheType: imageCacheFolder,
src: icon,
scale,
// backgroundImage overrides backgroundColor
backgroundColor: backgroundImage ? 'transparent' : backgroundColor ?? 'transparent',
borderRadiusRatio,
isAdaptive
});
if (backgroundImage) {
const backgroundLayer = await generateIconAsync(projectRoot, {
cacheType: backgroundImageCacheFolder,
src: backgroundImage,
scale,
backgroundColor: 'transparent',
borderRadiusRatio,
isAdaptive
});
if (backgroundImageFileName) {
await _fs().default.promises.writeFile(_path().default.resolve(dpiFolder, backgroundImageFileName), backgroundLayer);
} else {
iconLayer = await (0, _imageUtils().compositeImagesAsync)({
foreground: iconLayer,
background: backgroundLayer
});
}
} else if (backgroundImageFileName) {
// Remove any instances of ic_launcher_background.png that are there from previous icons
await deleteIconNamedAsync(projectRoot, backgroundImageFileName);
}
await _fs().default.promises.mkdir(dpiFolder, {
recursive: true
});
await _fs().default.promises.writeFile(_path().default.resolve(dpiFolder, outputImageFileName), iconLayer);
});
}
async function generateMonochromeImageAsync(projectRoot, {
icon,
imageCacheFolder,
outputImageFileName
}) {
await iterateDpiValues(projectRoot, async ({
dpiFolder,
scale
}) => {
const monochromeIcon = await generateIconAsync(projectRoot, {
cacheType: imageCacheFolder,
src: icon,
scale,
backgroundColor: 'transparent',
isAdaptive: true
});
await _fs().default.promises.mkdir(dpiFolder, {
recursive: true
});
await _fs().default.promises.writeFile(_path().default.resolve(dpiFolder, outputImageFileName), monochromeIcon);
});
}
function iterateDpiValues(projectRoot, callback) {
return Promise.all(Object.values(dpiValues).map(value => callback({
dpiFolder: _path().default.resolve(projectRoot, ANDROID_RES_PATH, value.folderName),
...value
})));
}
async function deleteIconNamedAsync(projectRoot, name) {
return iterateDpiValues(projectRoot, ({
dpiFolder
}) => {
return _fs().default.promises.rm(_path().default.resolve(dpiFolder, name), {
force: true
});
});
}
async function generateIconAsync(projectRoot, {
cacheType,
src,
scale,
backgroundColor,
borderRadiusRatio,
isAdaptive
}) {
const iconSizePx = (isAdaptive ? ADAPTIVE_BASELINE_PIXEL_SIZE : LEGACY_BASELINE_PIXEL_SIZE) * scale;
return (await (0, _imageUtils().generateImageAsync)({
projectRoot,
cacheType
}, {
src,
width: iconSizePx,
height: iconSizePx,
resizeMode: 'cover',
backgroundColor,
borderRadius: borderRadiusRatio ? iconSizePx * borderRadiusRatio : undefined
})).source;
}
//# sourceMappingURL=withAndroidIcons.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { AndroidConfig, ConfigPlugin } from '@expo/config-plugins';
import { ExpoConfig } from '@expo/config-types';
export declare const withAndroidManifestIcons: ConfigPlugin;
export declare function setRoundIconManifest(config: Pick<ExpoConfig, 'android'>, manifest: AndroidConfig.Manifest.AndroidManifest): AndroidConfig.Manifest.AndroidManifest;

View File

@@ -0,0 +1,30 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.setRoundIconManifest = setRoundIconManifest;
exports.withAndroidManifestIcons = void 0;
function _configPlugins() {
const data = require("@expo/config-plugins");
_configPlugins = function () {
return data;
};
return data;
}
const withAndroidManifestIcons = config => (0, _configPlugins().withAndroidManifest)(config, config => {
config.modResults = setRoundIconManifest(config, config.modResults);
return config;
});
exports.withAndroidManifestIcons = withAndroidManifestIcons;
function setRoundIconManifest(config, manifest) {
const isAdaptive = !!config.android?.adaptiveIcon;
const application = _configPlugins().AndroidConfig.Manifest.getMainApplicationOrThrow(manifest);
if (isAdaptive) {
application.$['android:roundIcon'] = '@mipmap/ic_launcher_round';
} else {
delete application.$['android:roundIcon'];
}
return manifest;
}
//# sourceMappingURL=withAndroidManifestIcons.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"withAndroidManifestIcons.js","names":["_configPlugins","data","require","withAndroidManifestIcons","config","withAndroidManifest","modResults","setRoundIconManifest","exports","manifest","isAdaptive","android","adaptiveIcon","application","AndroidConfig","Manifest","getMainApplicationOrThrow","$"],"sources":["../../../src/plugins/icons/withAndroidManifestIcons.ts"],"sourcesContent":["import { AndroidConfig, ConfigPlugin, withAndroidManifest } from '@expo/config-plugins';\nimport { ExpoConfig } from '@expo/config-types';\n\nexport const withAndroidManifestIcons: ConfigPlugin = (config) =>\n withAndroidManifest(config, (config) => {\n config.modResults = setRoundIconManifest(config, config.modResults);\n return config;\n });\n\nexport function setRoundIconManifest(\n config: Pick<ExpoConfig, 'android'>,\n manifest: AndroidConfig.Manifest.AndroidManifest\n): AndroidConfig.Manifest.AndroidManifest {\n const isAdaptive = !!config.android?.adaptiveIcon;\n const application = AndroidConfig.Manifest.getMainApplicationOrThrow(manifest);\n\n if (isAdaptive) {\n application.$['android:roundIcon'] = '@mipmap/ic_launcher_round';\n } else {\n delete application.$['android:roundIcon'];\n }\n return manifest;\n}\n"],"mappings":";;;;;;;AAAA,SAAAA,eAAA;EAAA,MAAAC,IAAA,GAAAC,OAAA;EAAAF,cAAA,YAAAA,CAAA;IAAA,OAAAC,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AAGO,MAAME,wBAAsC,GAAIC,MAAM,IAC3D,IAAAC,oCAAmB,EAACD,MAAM,EAAGA,MAAM,IAAK;EACtCA,MAAM,CAACE,UAAU,GAAGC,oBAAoB,CAACH,MAAM,EAAEA,MAAM,CAACE,UAAU,CAAC;EACnE,OAAOF,MAAM;AACf,CAAC,CAAC;AAACI,OAAA,CAAAL,wBAAA,GAAAA,wBAAA;AAEE,SAASI,oBAAoBA,CAClCH,MAAmC,EACnCK,QAAgD,EACR;EACxC,MAAMC,UAAU,GAAG,CAAC,CAACN,MAAM,CAACO,OAAO,EAAEC,YAAY;EACjD,MAAMC,WAAW,GAAGC,8BAAa,CAACC,QAAQ,CAACC,yBAAyB,CAACP,QAAQ,CAAC;EAE9E,IAAIC,UAAU,EAAE;IACdG,WAAW,CAACI,CAAC,CAAC,mBAAmB,CAAC,GAAG,2BAA2B;EAClE,CAAC,MAAM;IACL,OAAOJ,WAAW,CAACI,CAAC,CAAC,mBAAmB,CAAC;EAC3C;EACA,OAAOR,QAAQ;AACjB","ignoreList":[]}

View File

@@ -0,0 +1,13 @@
import { ConfigPlugin } from '@expo/config-plugins';
import { ExpoConfig, IOSIcons } from '@expo/config-types';
import { ContentsJsonImage } from './AssetContents';
export declare const withIosIcons: ConfigPlugin;
export declare function getIcons(config: Pick<ExpoConfig, 'icon' | 'ios'>): IOSIcons | string | null;
export declare function setIconsAsync(config: ExpoConfig, projectRoot: string): Promise<void>;
export declare function generateUniversalIconAsync(projectRoot: string, { icon, cacheKey, iosNamedProjectRoot, platform, appearance, }: {
platform: 'watchos' | 'ios';
icon?: string | null;
appearance?: 'dark' | 'tinted';
iosNamedProjectRoot: string;
cacheKey: string;
}): Promise<ContentsJsonImage>;

View File

@@ -0,0 +1,271 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.generateUniversalIconAsync = generateUniversalIconAsync;
exports.getIcons = getIcons;
exports.setIconsAsync = setIconsAsync;
exports.withIosIcons = void 0;
function _configPlugins() {
const data = require("@expo/config-plugins");
_configPlugins = function () {
return data;
};
return data;
}
function _Target() {
const data = require("@expo/config-plugins/build/ios/Target");
_Target = function () {
return data;
};
return data;
}
function _imageUtils() {
const data = require("@expo/image-utils");
_imageUtils = function () {
return data;
};
return data;
}
function _fs() {
const data = _interopRequireDefault(require("fs"));
_fs = function () {
return data;
};
return data;
}
function _path() {
const data = _interopRequireDefault(require("path"));
_path = function () {
return data;
};
return data;
}
function _AssetContents() {
const data = require("./AssetContents");
_AssetContents = function () {
return data;
};
return data;
}
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const {
getProjectName
} = _configPlugins().IOSConfig.XcodeUtils;
const IMAGE_CACHE_NAME = 'icons';
const IMAGESET_PATH = 'Images.xcassets/AppIcon.appiconset';
const withIosIcons = config => {
config = (0, _configPlugins().withDangerousMod)(config, ['ios', async config => {
await setIconsAsync(config, config.modRequest.projectRoot);
return config;
}]);
config = (0, _configPlugins().withXcodeProject)(config, config => {
const icon = getIcons(config);
const projectName = config.modRequest.projectName;
if (icon && typeof icon === 'string' && _path().default.extname(icon) === '.icon' && projectName) {
const iconName = _path().default.basename(icon, '.icon');
setIconName(config.modResults, projectName, iconName);
addIconFileToProject(config.modResults, projectName, iconName);
}
return config;
});
return config;
};
exports.withIosIcons = withIosIcons;
function getIcons(config) {
const iosSpecificIcons = config.ios?.icon;
if (iosSpecificIcons) {
// For backwards compatibility, the icon can be a string
if (typeof iosSpecificIcons === 'string') {
return iosSpecificIcons || config.icon || null;
}
if (typeof iosSpecificIcons === 'object') {
const paths = [iosSpecificIcons.light, iosSpecificIcons.dark, iosSpecificIcons.tinted].filter(Boolean);
for (const iconPath of paths) {
if (typeof iconPath === 'string' && _path().default.extname(iconPath) === '.icon') {
_configPlugins().WarningAggregator.addWarningIOS('icon', `Liquid glass icons (.icon) should be provided as a string to the "ios.icon" property, not as an object. Found: "${iconPath}"`);
}
}
}
// in iOS 18 introduced the ability to specify dark and tinted icons, which users can specify as an object
if (!iosSpecificIcons.light && !iosSpecificIcons.dark && !iosSpecificIcons.tinted) {
return config.icon || null;
}
return iosSpecificIcons;
}
// Top level icon property should not be used to specify a `.icon` folder
if (config.icon && typeof config.icon === 'string' && _path().default.extname(config.icon) === '.icon') {
_configPlugins().WarningAggregator.addWarningIOS('icon', `Liquid glass icons (.icon) should be provided via the "ios.icon" property, not the root "icon" property. Found: "${config.icon}"`);
}
if (config.icon) {
return config.icon;
}
return null;
}
async function setIconsAsync(config, projectRoot) {
const icon = getIcons(config);
if (!icon || typeof icon === 'string' && !icon || typeof icon === 'object' && !icon?.light && !icon?.dark && !icon?.tinted) {
_configPlugins().WarningAggregator.addWarningIOS('icon', 'No icon is defined in the Expo config.');
}
// Something like projectRoot/ios/MyApp/
const iosNamedProjectRoot = getIosNamedProjectPath(projectRoot);
if (typeof icon === 'string' && _path().default.extname(icon) === '.icon') {
return await addLiquidGlassIcon(icon, projectRoot, iosNamedProjectRoot);
}
// Ensure the Images.xcassets/AppIcon.appiconset path exists
await _fs().default.promises.mkdir(_path().default.join(iosNamedProjectRoot, IMAGESET_PATH), {
recursive: true
});
const imagesJson = [];
const baseIconPath = typeof icon === 'object' ? icon?.light || icon?.dark || icon?.tinted : icon;
// Store the image JSON data for assigning via the Contents.json
const baseIcon = await generateUniversalIconAsync(projectRoot, {
icon: baseIconPath,
cacheKey: 'universal-icon',
iosNamedProjectRoot,
platform: 'ios'
});
imagesJson.push(baseIcon);
if (typeof icon === 'object') {
if (icon?.dark) {
const darkIcon = await generateUniversalIconAsync(projectRoot, {
icon: icon.dark,
cacheKey: 'universal-icon-dark',
iosNamedProjectRoot,
platform: 'ios',
appearance: 'dark'
});
imagesJson.push(darkIcon);
}
if (icon?.tinted) {
const tintedIcon = await generateUniversalIconAsync(projectRoot, {
icon: icon.tinted,
cacheKey: 'universal-icon-tinted',
iosNamedProjectRoot,
platform: 'ios',
appearance: 'tinted'
});
imagesJson.push(tintedIcon);
}
}
// Finally, write the Contents.json
await (0, _AssetContents().writeContentsJsonAsync)(_path().default.join(iosNamedProjectRoot, IMAGESET_PATH), {
images: imagesJson
});
}
/**
* Return the project's named iOS path: ios/MyProject/
*
* @param projectRoot Expo project root path.
*/
function getIosNamedProjectPath(projectRoot) {
const projectName = getProjectName(projectRoot);
return _path().default.join(projectRoot, 'ios', projectName);
}
function getAppleIconName(size, scale, appearance) {
let name = 'App-Icon';
if (appearance) {
name = `${name}-${appearance}`;
}
name = `${name}-${size}x${size}@${scale}x.png`;
return name;
}
async function generateUniversalIconAsync(projectRoot, {
icon,
cacheKey,
iosNamedProjectRoot,
platform,
appearance
}) {
const size = 1024;
const filename = getAppleIconName(size, 1, appearance);
let source;
if (icon) {
// Using this method will cache the images in `.expo` based on the properties used to generate them.
// this method also supports remote URLs and using the global sharp instance.
source = (await (0, _imageUtils().generateImageAsync)({
projectRoot,
cacheType: IMAGE_CACHE_NAME + cacheKey
}, {
src: icon,
name: filename,
width: size,
height: size,
// Transparency needs to be preserved in dark variant, but can safely be removed in "light" and "tinted" variants.
removeTransparency: appearance !== 'dark',
// The icon should be square, but if it's not then it will be cropped.
resizeMode: 'cover',
// Force the background color to solid white to prevent any transparency. (for "any" and "tinted" variants)
// TODO: Maybe use a more adaptive option based on the icon color?
backgroundColor: appearance !== 'dark' ? '#ffffff' : undefined
})).source;
} else {
// Create a white square image if no icon exists to mitigate the chance of a submission failure to the app store.
source = await (0, _imageUtils().createSquareAsync)({
size
});
}
// Write image buffer to the file system.
const assetPath = _path().default.join(iosNamedProjectRoot, IMAGESET_PATH, filename);
await _fs().default.promises.writeFile(assetPath, source);
return {
filename,
idiom: 'universal',
platform,
size: `${size}x${size}`,
...(appearance ? {
appearances: [{
appearance: 'luminosity',
value: appearance
}]
} : {})
};
}
async function addLiquidGlassIcon(iconPath, projectRoot, iosNamedProjectRoot) {
const iconName = _path().default.basename(iconPath, '.icon');
const sourceIconPath = _path().default.join(projectRoot, iconPath);
const targetIconPath = _path().default.join(iosNamedProjectRoot, `${iconName}.icon`);
if (!_fs().default.existsSync(sourceIconPath)) {
_configPlugins().WarningAggregator.addWarningIOS('icon', `Liquid glass icon file not found at path: ${iconPath}`);
return;
}
await _fs().default.promises.cp(sourceIconPath, targetIconPath, {
recursive: true
});
}
/**
* Adds the .icons name to the project
*/
function setIconName(project, projectName, iconName) {
const [, target] = (0, _Target().findNativeTargetByName)(project, projectName);
const configurations = _configPlugins().IOSConfig.XcodeUtils.getBuildConfigurationsForListId(project, target.buildConfigurationList);
for (const [, config] of configurations) {
if (config?.buildSettings) {
config.buildSettings.ASSETCATALOG_COMPILER_APPICON_NAME = iconName;
}
}
}
/**
* Adds the .icon file to the project
*/
function addIconFileToProject(project, projectName, iconName) {
const iconPath = `${iconName}.icon`;
_configPlugins().IOSConfig.XcodeUtils.addResourceFileToGroup({
filepath: `${projectName}/${iconPath}`,
groupName: projectName,
project,
isBuildFile: true,
verbose: true
});
}
//# sourceMappingURL=withIosIcons.js.map

File diff suppressed because one or more lines are too long