Repository: martiliones/icon-set-creator
Branch: main
Commit: 82aefc317f87
Files: 18
Total size: 42.0 KB
Directory structure:
gitextract_nlmhsx1m/
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── index.ts
├── lib/
│ ├── creator/
│ │ ├── android.ts
│ │ ├── index.ts
│ │ └── ios.ts
│ └── utils/
│ ├── android.ts
│ └── ios.ts
├── package.json
├── tsconfig.json
└── utils/
├── dir.ts
├── enhanceErrorMessages.ts
├── index.ts
├── logger.ts
└── pkg.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules
tests/**/output
coverage
AppTestName
dist
================================================
FILE: .npmignore
================================================
.eslintrc.js
example
tests
assets
.circleci
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# [1.1.6] (2023-08-12)
### Fixed
- Overwriting ASSETCATALOG* build settings for iOS ([#25])
# [1.0.0] (2022-05-29)
### Added
- Support for JSON config file (`.iconsetrc.json`)
- Generating the round icons for Android by default ([#7])
### Fixed
- Merge config parameters with CLI options ([#17])
### Changed
- The name for config file now is `.iconsetrc.js` instead of `iconset.config.js`
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021-2023 martiliones
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
[1]: https://www.npmjs.com/package/icon-set-creator
Icon Set Creator
Android & iOS icon generator for React Native
- 🌈 Easy to install — does not require additional software
- ⚡️ Fast — image manipulation powered by [sharp](https://github.com/lovell/sharp)
- 🛠 Configurable — using cli options or config file
- 📱 iOS and Android support — create icons for both platforms with one command
- 🌟 Adaptive Icons — support for color and image backgrounds
- 🟢 Round Icons — automatically generated for Android
⚡️ Quick Start
You can run the icon generator with the npx command (available in Node.js 8.2.0 and later).
```bash
$ npx icon-set-creator create ./path/to/icon.png
```
For earlier Node versions, see [🚀 Installation](#-installation) section below.
🚀 Installation
> **Node Version Requirement**
>
> Icon set creator requires Node.js version 14.0 or above (v16+ recommended). You can manage multiple versions of Node on the same machine with [n](https://github.com/tj/n), [nvm](https://github.com/creationix/nvm) or [nvm-windows](https://github.com/coreybutler/nvm-windows) .
Global
To install the new package **globally**, use one of the following commands. You need administrator privileges to execute these unless npm was installed on your system through a Node.js version manager (e.g. n or nvm).
```bash
$ npm install -g icon-set-creator
# OR
$ yarn global add icon-set-creator
```
After installation, you will have access to the iconset binary in your command line. You can verify that it is properly installed by simply running `iconset`, which should present you with a help message listing all available commands.
You can check you have the right version with this command:
```bash
$ iconset --version
```
Local for a project
If you want to install the [`icon-set-creator`][1] **locally**, use one of the following commands:
```bash
$ npm install icon-set-creator -D
# OR
$ yarn add icon-set-creator -D
```
🧪 Usage
To create app icon you need:
- PNG icon for IOS and Android (Highly recommend using an icon with a size of at least 1024x1024 pixels). You can check the [`example`](https://github.com/martiliones/icon-set-creator/tree/master/example) folder for example icons.
- You can also create Adaptive Icon for Android, which can display a variety of shapes across different device models ([Learn More](https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive)). To create it you need a foreground image and a background color or image. [There is also a good article](https://medium.com/google-design/designing-adaptive-icons-515af294c783) on how to design such icons.
The easiest way to use [`icon-set-creator`][1] is to specify the path to icon using `iconset create` command in root of your project:
```bash
$ iconset create ./icon.png
```
If you have the package installed locally, you can do same with the `package.json` script and then run it with `npm run create-appicon`:
```json5
{
"scripts": {
"create-appicon": "iconset create ./icon.png"
}
}
```
It will generate icons of different sizes for Android and iOS.
⚙️ Configuration
There are two primary ways to configure [`icon-set-creator`][1]:
- **CLI parameters** - use the command options.
- **Configuration files** - use a JavaScript, JSON, or `package.json` file to specify configuration information to generate an application icon depending on your code style.
CLI parameters
To display all of the options for the given command, run `iconset --help`. For example:
```bash
$ iconset create --help
Usage: index create [options] [image-path]
Generate a new icon set for React Native project
Options:
-d, --disable-launcher-icon Disable changing the launcher icon for iOS and Android
-A, --android [icon-name] Generate icon set for android
-IPA, --image-path-android Image path for android
--flavor [flavor] Flavor name
-b, --adaptive-icon-background The color (E.g. "#ffffff") or image asset (E.g. "assets/images/christmas-background.png") which will be
used to fill out the background of the adaptive icon.
-f, --adaptive-icon-foreground The image asset which will be used for the icon foreground of the adaptive icon
-I, --ios Generate icon set for ios
--group Group for ios
-IPI, --image-path-ios Image path for ios
-h, --help display help for command
```
Configuration files
[`icon-set-creator`][1] supports configuration files in several formats:
- JavaScript - use `.iconsetrc.js` and export an object containing your configuration.
- JSON - use `.iconsetrc.json` to define the configuration structure.
- `package.json` - create an `iconsetConfig` property in your package.json file and define your configuration there.
If there are multiple configuration files in the same directory, `icon-set-creator` will only use one. The priority order is as follows:
- `.iconsetrc.js`
- `.iconsetrc.json`
- `package.json`
[`icon-set-creator`][1] will automatically look for them in the directory path to be used to run the CLI.
Here's an example JavaScript configuration file that uses the `adaptiveIconBackground`/`adaptiveIconForeground` options to support adaptive icons:
```js
// .iconsetrc.js
module.exports = {
imagePath: './assets/icon.png',
adaptiveIconBackground: './assets/icon-background.png',
adaptiveIconForeground: './assets/icon-foreground.png',
};
```
iconset create
- `imagePath` — The location of the icon image file which you want to use as the app launcher icon. e.g. `./assets/icon.png`
- `disableLauncherIcon` - Generate only icons without changing manifest files
- `android`/`ios` (optional): `true` — Override the default existing React-Native launcher icon for the platform specified, `false` — ignore making launcher icons for this platform, `icon_name` — this will generate a new launcher icons for the platform with the name you specify, without removing the old default existing React-Native launcher icon.
- `imagePathAndroid` — The location of the icon image file specific for Android platform (optional — if not defined then the `imagePath` is used)
- `imagePathIos` — The location of the icon image file specific for iOS platform (optional — if not defined then the `imagePath` is used)
The next two attributes are only used when generating Android launcher icon:
- `adaptiveIconBackground` — The color (E.g. `"#ffffff"`) or image asset (E.g. `"assets/images/dark-background.png"`) which will be used to fill out the background of the adaptive icon
- `adaptiveIconForeground` — The image asset which will be used for the icon foreground of the adaptive icon
✨ You are amazing!
================================================
FILE: index.ts
================================================
#!/usr/bin/env node
import leven from 'leven';
import minimist from 'minimist';
import { Command, program } from 'commander';
import IconCreator from './lib/creator/index';
import { chalk, semver } from './utils/index';
import { engines } from './package.json';
const requiredVersion = engines.node;
const checkNodeVersion = (wanted: string, id: string) => {
if (!semver.satisfies(process.version, wanted, { includePrerelease: true })) {
console.log(chalk.red(
'You are using Node ' + process.version + ', but this version of ' + id +
' requires Node ' + wanted + '.\nPlease upgrade your Node version.'
));
process.exit(1);
}
};
// Check node version before requiring/doing anything else
// The user may be on a very old node version
checkNodeVersion(requiredVersion, 'icon-set-creator');
const suggestCommands = (unknownCommand: string) => {
const availableCommands = program.commands.map((cmd: Command) => cmd.name());
let suggestion = '';
availableCommands.forEach((cmd: string) => {
const isBestMatch = leven(cmd, unknownCommand) < leven(suggestion || '', unknownCommand);
if (leven(cmd, unknownCommand) < 3 && isBestMatch) {
suggestion = cmd;
}
});
if (suggestion) {
console.log(' ' + chalk.red(`Did you mean ${chalk.yellow(suggestion)}?`));
}
};
program
.version(`icon-set-creator ${require('./package').version}`)
.usage(' [options]');
program
.command('create [image-path]')
.description('Generate a new icon set for React Native project')
.option('-d, --disable-launcher-icon', 'Disable changing the launcher icon for iOS and Android')
.option('-A, --android [icon-name]', 'Generate icon set for android')
.option('-IPA, --image-path-android', 'Image path for android')
.option('--flavor [flavor]', 'Flavor name')
.option('-b, --adaptive-icon-background ', 'The color (E.g. "#ffffff") or image asset (E.g. "assets/images/christmas-background.png") which will be used to fill out the background of the adaptive icon.')
.option('-f, --adaptive-icon-foreground ', 'The image asset which will be used for the icon foreground of the adaptive icon')
.option('-I, --ios', 'Generate icon set for ios')
.option('--group ', 'Group for ios')
.option('-IPI, --image-path-ios', 'Image path for ios')
.action((imagePath: string, options) => {
if (minimist(process.argv.slice(3))._.length > 1) {
console.log(chalk.yellow('\n Info: You provided more than one argument. The first one will be used as the source file, the rest are ignored.'));
}
const iconCreator = new IconCreator({ ...options, imagePath });
iconCreator.run();
});
program
.command('remove')
.description('remove a icon set from React Native project')
.option('-A, --android', 'remove icon set for android')
.option('-I, --ios', 'remove icon set for ios')
.action((options) => {
console.log(options);
// require('../lib/remove')(options);
});
// output help information on unknown commands
program.on('command:*', ([cmd]) => {
program.outputHelp();
console.log(' ' + chalk.red(`Unknown command ${chalk.yellow(cmd)}.`));
console.log();
suggestCommands(cmd);
process.exitCode = 1;
});
// add some useful info on help
program.on('--help', () => {
console.log();
console.log(` Run ${chalk.cyan('iconset --help')} for detailed usage of given command.`);
console.log();
});
program.commands.forEach(c => c.on('--help', () => console.log()));
// enhance common error messages
import enhanceErrorMessages from './utils/enhanceErrorMessages';
enhanceErrorMessages('missingArgument', (argName: string) => {
return `Missing required argument ${chalk.yellow(`<${argName}>`)}.`;
});
enhanceErrorMessages('unknownOption', (optionName: string) => {
return `Unknown option ${chalk.yellow(optionName)}.`;
});
enhanceErrorMessages('optionMissingArgument', (option: { flags: any; }, flag: string) => {
return `Missing required argument for option ${chalk.yellow(option.flags)}` + (
flag ? `, got ${chalk.yellow(flag)}` : ''
);
});
program.parse(process.argv);
================================================
FILE: lib/creator/android.ts
================================================
import sharp from 'sharp';
import fs from 'fs';
import path from 'path';
import { log, warn, createDirectory } from '../../utils/index';
import {
getAndroidResDirectory,
getAndroidAdaptiveXmlFolder,
getAndroidColorsFile,
isAndroidIconNameCorrectFormat,
getIcLauncherDrawableBackgroundXml,
getIcLauncherXml,
getColorsXmlTemplate,
getRoundedCornersLayer,
androidIcons,
adaptiveAndroidIcons,
androidManifestFile,
androidAdaptiveForegroundFileName,
androidAdaptiveBackgroundFileName,
AndroidIcon,
} from '../utils/android';
interface AndroidCreatorOptions {
flavor?: string;
android?: any;
disableLauncherIcon?: boolean;
}
class AndroidIconCreator {
context: string;
options: AndroidCreatorOptions;
constructor(context: string, opts: AndroidCreatorOptions) {
this.context = context;
this.options = opts;
if (typeof opts.android === 'string') {
if (!isAndroidIconNameCorrectFormat(opts.android)) {
throw new Error('The icon name must contain only lowercase a-z, 0-9, or underscore: \nE.g. "ic_my_new_icon"');
}
}
}
createAndroidIcons(imagePath: string): Promise {
return new Promise((resolve, reject) => {
const androidResDirectory = path.resolve(this.context, getAndroidResDirectory(this.options.flavor));
let iconName = this.options.android;
if (typeof iconName === 'string') {
log('🚀 Adding a new Android launcher icon');
} else {
iconName = 'ic_launcher';
log('Overwriting the default Android launcher icon with a new icon');
}
fs.readFile(imagePath, async (err, image) => {
if (err) {
return reject(err);
}
if (!this.options.disableLauncherIcon) {
await this.overwriteAndroidManifestIcon(iconName!);
}
for (const androidIcon of androidIcons) {
const iconDirectory = path.resolve(androidResDirectory, androidIcon.directoryName);
await this.saveIcon(image, iconDirectory, iconName, androidIcon);
await this.saveRoundedIcon(image, iconDirectory, iconName, androidIcon);
}
sharp(image)
.resize(512, 512)
.toFile(path.resolve(androidResDirectory, 'playstore-icon.png'), (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
});
};
createAdaptiveIcons(adaptiveIconBackground: string, adaptiveIconForeground: string): Promise {
const { flavor, android } = this.options;
return new Promise((resolve, reject) => {
fs.readFile(path.resolve(this.context, adaptiveIconForeground), async (err, foreground) => {
if (err) {
return reject(err);
}
const androidResDirectory = path.resolve(this.context, getAndroidResDirectory(flavor));
const foregroundIconName = typeof android === 'string'
? `${android}_foreground` : androidAdaptiveForegroundFileName;
for (const adaptiveIcon of adaptiveAndroidIcons) {
const iconDirectory = path.resolve(androidResDirectory, adaptiveIcon.directoryName);
await this.saveIcon(foreground, iconDirectory, foregroundIconName, adaptiveIcon);
}
if (path.extname(adaptiveIconBackground) === '.png') {
await this.createAdaptiveBackgrounds(adaptiveIconBackground, androidResDirectory);
} else {
await this.createAdaptiveIconMipmapXmlFile();
await this.updateColorsXmlFile(adaptiveIconBackground);
}
resolve();
});
});
}
saveRoundedIcon(image: Buffer, iconDirectory: string, iconName: string, androidIcon: AndroidIcon) {
const roundIconName = `${iconName}_round`;
const { size } = androidIcon;
return new Promise((resolve, reject) => {
sharp(image)
.resize(size, size)
.composite([{
input: getRoundedCornersLayer(size),
blend: 'dest-in'
}])
.toFile(path.resolve(iconDirectory, `${roundIconName}.png`), (err, info) => {
if (err) {
return reject(err);
}
resolve(info);
});
});
}
saveIcon(image: Buffer, iconDirectory: string, iconName: string, androidIcon: AndroidIcon) {
return new Promise((resolve, reject) => {
createDirectory(iconDirectory);
sharp(image)
.resize(androidIcon.size, androidIcon.size)
.toFile(path.resolve(iconDirectory, `${iconName}.png`), (err, info) => {
if (err) {
return reject(err);
}
resolve(info);
});
});
}
updateColorsXmlFile(adaptiveIconBackground: string) {
const { flavor } = this.options;
return new Promise((resolve) => {
const colorsXml = path.resolve(this.context, getAndroidColorsFile(flavor));
if (fs.existsSync(colorsXml)) {
log('📄 Updating colors.xml with color for adaptive icon background');
resolve(this.updateColorsFile(colorsXml, adaptiveIconBackground));
} else {
log('⚠️ No colors.xml file found in your Android project');
log('Creating colors.xml file and adding it to your Android project');
resolve(this.createNewColorsFile(adaptiveIconBackground));
}
});
}
updateColorsFile(colorsXml: string, adaptiveIconBackground: string): Promise {
return new Promise((resolve, reject) => {
fs.readFile(colorsXml, 'utf-8', (err, colors) => {
if (err) {
return reject(err);
}
const lines = colors.split('\n');
let foundExisting = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.includes('name="ic_launcher_background"')) {
foundExisting = true;
// replace anything between tags which does not contain another tag
lines[i] = line.replace(/>([^><]*)${adaptiveIconBackground}<`);
break;
}
}
if (!foundExisting) {
lines.splice(lines.length - 1, 0,
`\t${adaptiveIconBackground}`);
}
fs.writeFileSync(colorsXml, lines.join('\n'));
resolve();
});
});
}
createNewColorsFile(adaptiveIconBackground: string) {
return new Promise((resolve) => {
const colorsXml = path.resolve(this.context, getAndroidColorsFile(this.options.flavor));
createDirectory(path.dirname(colorsXml));
const { android } = this.options;
const iconName = typeof android === 'string'
? android : 'ic_launcher';
fs.writeFileSync(colorsXml, getColorsXmlTemplate(iconName));
resolve(this.updateColorsFile(colorsXml, adaptiveIconBackground));
});
}
createAdaptiveIconMipmapXmlFile(): Promise {
const { android, flavor } = this.options;
return new Promise((resolve, reject) => {
const iconName = typeof android === 'string'
? android : 'ic_launcher';
const iconFileName = `${iconName}.xml`;
const directory = path.resolve(this.context, getAndroidAdaptiveXmlFolder(flavor));
createDirectory(directory);
fs.writeFile(path.resolve(directory, iconFileName), getIcLauncherXml(iconName), (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
}
createAdaptiveBackgrounds(adaptiveIconBackground: string, androidResDirectory: string): Promise {
const { android, flavor } = this.options;
const adaptiveIconBackgroundPath = path.resolve(this.context, adaptiveIconBackground);
return new Promise((resolve, reject) => {
fs.readFile(adaptiveIconBackgroundPath, async (err, background) => {
if (err) {
return reject(err);
}
const backgroundIconName = typeof android === 'string'
? `${android}_background` : androidAdaptiveBackgroundFileName;
for (const adaptiveIcon of adaptiveAndroidIcons) {
const iconDirectory = path.resolve(androidResDirectory, adaptiveIcon.directoryName);
await this.saveIcon(background, iconDirectory, backgroundIconName, adaptiveIcon);
}
const iconName = typeof android === 'string'
? android : 'ic_launcher';
const directory = path.resolve(this.context, getAndroidAdaptiveXmlFolder(flavor));
createDirectory(directory);
fs.writeFile(path.resolve(directory, `${iconName}.xml`), getIcLauncherDrawableBackgroundXml(iconName), (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
});
}
overwriteAndroidManifestIcon(iconName: string): Promise {
return new Promise((resolve, reject) => {
fs.readFile(androidManifestFile, 'utf-8', (err, manifest) => {
if (err) {
if (err.code === 'ENOENT') {
warn('No AndroidManifest.xml was found, icon can\'t be replaced. Skipped');
return resolve();
}
return reject(err);
}
log('Overwriting icon in AndroidManifest.xml');
const newManifest = this.transformAndroidManifestIcon(manifest, iconName);
fs.writeFile(androidManifestFile, newManifest, (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
});
}
transformAndroidManifestIcon(oldManifest: string, iconName: string) {
return oldManifest.split('\n').map((line) => {
if (line.includes('android:icon')) {
// Using RegExp replace the value of android:icon to point to the new icon
// anything but a quote of any length: [^"]*
// an escaped quote: \\" (escape slash, because it exists regex)
// quote, no quote / quote with things behind : \"[^"]*
// repeat as often as wanted with no quote at start: [^"]*(\"[^"]*)*
// escaping the slash to place in string: [^"]*(\\"[^"]*)*"
// result: any string which does only include escaped quotes
return line.replace(/android:icon="[^"]*(\\"[^"]*)*"/g,
`android:icon="@mipmap/${iconName}"`);
} else if (line.includes('android:roundIcon')) {
return line.replace(/android:icon="[^"]*(\\"[^"]*)*"/g,
`android:roundIcon="@mipmap/${iconName}_round"`);
} else {
return line;
}
}).join('\n');
}
}
export default AndroidIconCreator;
================================================
FILE: lib/creator/index.ts
================================================
import path from 'path';
import { resolveConfig, error, info, log } from '../../utils/index';
import IOSIconCreator from './ios';
import AndroidIconCreator from './android';
interface creatorOptions {
imagePath?: string;
android?: boolean | string;
ios?: boolean | string;
imagePathAndroid?: string;
imagePathIos?: string;
flavor?: string;
adaptiveIconBackground?: string;
adaptiveIconForeground?: string;
group?: string;
disableLauncherIcon?: boolean;
};
export default class Creator {
options: creatorOptions;
context: string;
constructor(opts: creatorOptions) {
const context = process.cwd();
const config = resolveConfig(context);
if (!opts.imagePath) {
opts.imagePath = config.imagePath;
}
const options = {
...config,
...opts
};
// both android and ios are included by default
if (!options.android && !options.ios) {
options.android = true;
options.ios = true;
}
this.context = context;
this.options = options;
this.resovleOptionPaths();
}
resovleOptionPaths() {
const context = process.cwd();
const options = this.options;
const paths = [
'imagePath',
'imagePathIos',
'imagePathAndroid',
'adaptiveIconBackground',
'adaptiveIconForeground',
] as const;
for (const prop of paths) {
if (typeof options[prop] !== 'undefined') {
if (!options[prop]!.match(/^#[0-9A-Za-z]{6}$/)) {
options[prop] = path.resolve(context, options[prop]!);
}
}
}
}
async run() {
const options = this.options;
const context = this.context;
if (options.android) {
info('Creating icons for Android...');
const imagePathAndroid = options.imagePathAndroid || options.imagePath;
if (!imagePathAndroid) {
return error('No image path was specified for android');
}
const androidIconCreator = new AndroidIconCreator(context, {
flavor: options.flavor,
android: options.android,
disableLauncherIcon: options.disableLauncherIcon,
});
await androidIconCreator.createAndroidIcons(imagePathAndroid);
const { adaptiveIconBackground, adaptiveIconForeground } = options;
if (adaptiveIconBackground && adaptiveIconForeground) {
await androidIconCreator.createAdaptiveIcons(adaptiveIconBackground, adaptiveIconForeground);
}
}
if (options.ios) {
info('Creating icons for IOS...');
const iOSIconCreator = new IOSIconCreator(context, {
ios: options.ios,
flavor: options.flavor,
group: options.group,
disableLauncherIcon: options.disableLauncherIcon,
});
const imagePathIos = options.imagePathIos || options.imagePath;
if (!imagePathIos) {
return error('No image path was specified for iOS');
}
await iOSIconCreator.createIosIcons(imagePathIos!);
}
log();
log('🎉 Successfully generated icons.');
}
}
================================================
FILE: lib/creator/ios.ts
================================================
import sharp from 'sharp';
import fs from 'fs';
import path from 'path';
import { warn, createDirectory } from '../../utils/index';
import {
iosDefaultIconName,
iosDefaultCatalogName,
getIosDefaultIconFolder,
getIosConfigFile,
getIosAssetFolder,
generateContentsFile,
iosIcons,
IosIcon,
} from '../utils/ios';
interface IOSCreatorOptions {
ios?: boolean | string;
flavor?: string;
group?: string;
disableLauncherIcon?: boolean;
}
class IOSIconCreator {
context: string;
options: IOSCreatorOptions;
constructor(context: string, opts: IOSCreatorOptions) {
this.context = context;
this.options = opts;
}
createIosIcons(imagePath: string): Promise {
return new Promise((resolve, reject) => {
const projectName = this.getIosProjectName();
if (!projectName) {
return reject('No Project Directory for IOS was found');
}
fs.readFile(imagePath, async (err, image) => {
if (err) {
return reject(err);
}
let iconName = iosDefaultIconName;
let catalogName = iosDefaultCatalogName;
const { flavor, ios, disableLauncherIcon } = this.options;
if (flavor) {
catalogName = `AppIcon-${flavor}`;
iconName = iosDefaultIconName;
} else if (typeof ios === 'string') {
iconName = ios;
catalogName = iconName;
}
for (const iosIcon of iosIcons) {
const flavorPath = path.resolve(this.context, getIosDefaultIconFolder(projectName, flavor));
await this.saveIosIcon(
image,
flavorPath,
iconName,
iosIcon
);
}
if (!disableLauncherIcon) {
await this.changeIosLauncherIcon(catalogName, projectName);
}
this.modifyContentsFile(catalogName, iconName, projectName);
resolve();
});
});
}
getIosProjectName() {
const { group } = this.options;
if (group) {
return group;
}
const appFilePath = path.resolve(this.context, 'app.json');
if (fs.existsSync(appFilePath)) {
const app = require(appFilePath);
if (typeof app === 'object' && app.name) {
return app.name;
}
}
const iosDirectory = path.resolve(this.context, 'ios');
const directories = fs.readdirSync(iosDirectory, { withFileTypes: true });
for (const dir of directories) {
if (!dir.isDirectory()) {
continue;
}
if (fs.existsSync(path.resolve(iosDirectory, dir.name, 'Images.xcassets'))) {
return dir.name;
}
}
return 'AppName';
}
saveIosIcon(image: Buffer, iconDirectory: string, iconName: string, iosIcon: IosIcon) {
return new Promise((resolve, reject) => {
createDirectory(iconDirectory);
sharp(image)
.resize(iosIcon.size, iosIcon.size)
.removeAlpha() // Icons with alpha channel are not allowed in the Apple App Store
.toFile(path.resolve(iconDirectory, `${iconName + iosIcon.name}.png`), (err, data) => {
if (err) {
return reject(err);
}
resolve(data);
});
});
}
changeIosLauncherIcon(catalogName: string, projectName: string): Promise {
return new Promise((resolve, reject) => {
const iOSconfigFile = path.resolve(this.context, getIosConfigFile(projectName));
fs.readFile(iOSconfigFile, 'utf-8', (err, config) => {
if (err) {
if (err.code === 'ENOENT') {
warn('No project.pbxproj was found, icon can\'t be replaced in config file. Skipped');
return resolve();
}
return reject(err);
}
const lines = config.split('\n');
let currentConfig, onConfigurationSection = false;
for (let i = 0; i <= lines.length - 1; i++) {
const line = lines[i];
if (line.includes('/* Begin XCBuildConfiguration section */')) {
onConfigurationSection = true;
}
if (line.includes('/* End XCBuildConfiguration section */')) {
onConfigurationSection = false;
}
if (onConfigurationSection) {
const regex = /.*\/* (.*)\.xcconfig \*\/;/;
const match = regex.exec(line);
if (match) {
currentConfig = match[1];
}
if (currentConfig && line.includes('ASSETCATALOG_COMPILER_APPICON_NAME')) {
lines[i] = line.replace(/=(.*);/g, `= ${catalogName};`);
}
}
}
const entireFile = lines.join('\n');
resolve(fs.writeFileSync(iOSconfigFile, entireFile));
});
});
}
modifyContentsFile(newCatalogName: string, newIconName: string, projectName: string) {
const newIconDirectory = path.resolve(
this.context,
getIosAssetFolder(projectName),
`${newCatalogName}.appiconset/Contents.json`
);
createDirectory(path.dirname(newIconDirectory));
const contentsFileContent = generateContentsFile(newIconName);
fs.writeFileSync(newIconDirectory, JSON.stringify(contentsFileContent, null, 2));
}
}
export default IOSIconCreator;
================================================
FILE: lib/utils/android.ts
================================================
export interface AndroidIcon {
directoryName: string;
size: number;
}
export const androidManifestFile = 'android/app/src/main/AndroidManifest.xml';
export const getAndroidResDirectory = (flavor?: string) => `android/app/src/${flavor ?? 'main'}/res/`;
export const getAndroidAdaptiveXmlFolder = (flavor?: string) => `${getAndroidResDirectory(flavor)}mipmap-anydpi-v26/`;
export const getAndroidColorsFile = (flavor?: string) => `${getAndroidResDirectory(flavor)}/values/colors.xml`;
export const isAndroidIconNameCorrectFormat = (iconName: string) => {
return /^[a-z0-9_]+$/.exec(iconName);
};
export const androidAdaptiveForegroundFileName = 'ic_launcher_foreground';
export const androidAdaptiveBackgroundFileName = 'ic_launcher_background';
export const adaptiveAndroidIcons: AndroidIcon[] = [
{ directoryName: 'drawable-mdpi', size: 108 },
{ directoryName: 'drawable-hdpi', size: 162 },
{ directoryName: 'drawable-xhdpi', size: 216 },
{ directoryName: 'drawable-xxhdpi', size: 324 },
{ directoryName: 'drawable-xxxhdpi', size: 432 },
];
export const androidIcons: AndroidIcon[] = [
{ directoryName: 'mipmap-mdpi', size: 48 },
{ directoryName: 'mipmap-hdpi', size: 72 },
{ directoryName: 'mipmap-xhdpi', size: 96 },
{ directoryName: 'mipmap-xxhdpi', size: 144 },
{ directoryName: 'mipmap-xxxhdpi', size: 192 },
];
export const getRoundedCornersLayer = (size: number) => Buffer.from(
``
);
export const getIcLauncherXml = (iconName?: string) => `
`;
export const getIcLauncherDrawableBackgroundXml = (iconName?: string) => `
`;
export const getColorsXmlTemplate = (iconName?: string) => `
#FF000000
`;
================================================
FILE: lib/utils/ios.ts
================================================
export interface IosIcon {
name: string;
size: number;
}
export const iosDefaultIconName = 'Icon-App';
export const iosDefaultCatalogName = 'AppIcon';
export const getIosDefaultIconFolder = (projectName: string, flavor?: string) => (
`ios/${projectName}/Images.xcassets/AppIcon${flavor?`-${flavor}`:''}.appiconset/`
);
export const getIosConfigFile = (projectName: string) => `ios/${projectName}.xcodeproj/project.pbxproj`;
export const getIosAssetFolder = (projectName: string) => `ios/${projectName}/Images.xcassets/`;
export const iosIcons: IosIcon[] = [
{ name: '-20x20@1x', size: 20 },
{ name: '-20x20@2x', size: 40 },
{ name: '-20x20@3x', size: 60 },
{ name: '-29x29@1x', size: 29 },
{ name: '-29x29@2x', size: 58 },
{ name: '-29x29@3x', size: 87 },
{ name: '-40x40@1x', size: 40 },
{ name: '-40x40@2x', size: 80 },
{ name: '-40x40@3x', size: 120 },
{ name: '-60x60@2x', size: 120 },
{ name: '-60x60@3x', size: 180 },
{ name: '-76x76@1x', size: 76 },
{ name: '-76x76@2x', size: 152 },
{ name: '-83.5x83.5@2x', size: 167 },
{ name: '-1024x1024@1x', size: 1024 },
];
export const generateContentsFile = (newIconName: string) => ({
images: createImageList(newIconName),
info: { version: 1, author: 'xcode' },
});
export const createImageList = (newIconName: string) => {
return [
{
size: '20x20',
idiom: 'iphone',
filename: `${newIconName}-20x20@2x.png`,
scale: '2x'
},
{
size: '20x20',
idiom: 'iphone',
filename: `${newIconName}-20x20@3x.png`,
scale: '3x'
},
{
size: '29x29',
idiom: 'iphone',
filename: `${newIconName}-29x29@1x.png`,
scale: '1x'
},
{
size: '29x29',
idiom: 'iphone',
filename: `${newIconName}-29x29@2x.png`,
scale: '2x'
},
{
size: '29x29',
idiom: 'iphone',
filename: `${newIconName}-29x29@3x.png`,
scale: '3x'
},
{
size: '40x40',
idiom: 'iphone',
filename: `${newIconName}-40x40@2x.png`,
scale: '2x'
},
{
size: '40x40',
idiom: 'iphone',
filename: `${newIconName}-40x40@3x.png`,
scale: '3x'
},
{
size: '60x60',
idiom: 'iphone',
filename: `${newIconName}-60x60@2x.png`,
scale: '2x'
},
{
size: '60x60',
idiom: 'iphone',
filename: `${newIconName}-60x60@3x.png`,
scale: '3x'
},
{
size: '20x20',
idiom: 'ipad',
filename: `${newIconName}-20x20@1x.png`,
scale: '1x'
},
{
size: '20x20',
idiom: 'ipad',
filename: `${newIconName}-20x20@2x.png`,
scale: '2x'
},
{
size: '29x29',
idiom: 'ipad',
filename: `${newIconName}-29x29@1x.png`,
scale: '1x'
},
{
size: '29x29',
idiom: 'ipad',
filename: `${newIconName}-29x29@2x.png`,
scale: '2x'
},
{
size: '40x40',
idiom: 'ipad',
filename: `${newIconName}-40x40@1x.png`,
scale: '1x'
},
{
size: '40x40',
idiom: 'ipad',
filename: `${newIconName}-40x40@2x.png`,
scale: '2x'
},
{
size: '76x76',
idiom: 'ipad',
filename: `${newIconName}-76x76@1x.png`,
scale: '1x'
},
{
size: '76x76',
idiom: 'ipad',
filename: `${newIconName}-76x76@2x.png`,
scale: '2x'
},
{
size: '83.5x83.5',
idiom: 'ipad',
filename: `${newIconName}-83.5x83.5@2x.png`,
scale: '2x'
},
{
size: '1024x1024',
idiom: 'ios-marketing',
filename: `${newIconName}-1024x1024@1x.png`,
scale: '1x'
}
];
};
================================================
FILE: package.json
================================================
{
"name": "icon-set-creator",
"version": "1.2.6",
"description": "Android & iOS icon generator for React Native",
"main": "index.js",
"bin": {
"iconset": "index.js"
},
"author": "martiliones",
"license": "MIT",
"bugs": {
"url": "https://github.com/martiliones/icon-set-creator/issues"
},
"homepage": "https://github.com/martiliones/icon-set-creator#readme",
"engines": {
"node": ">=14"
},
"scripts": {
"build": "tsc --project tsconfig.json",
"lint": "eslint ./**/*.ts",
"lint:fix": "eslint ./**/*.ts --fix"
},
"repository": {
"type": "git",
"url": "git+https://github.com/martiliones/icon-set-creator.git"
},
"keywords": [
"cli",
"mobile",
"react-native",
"icons",
"android-studio",
"Xcode",
"icon-set-creator"
],
"devDependencies": {
"@tsconfig/recommended": "^1.0.1",
"@types/minimist": "^1.2.2",
"@types/node": "^17.0.45",
"@types/semver": "^7.3.9",
"@types/sharp": "^0.30.2",
"@typescript-eslint/eslint-plugin": "^5.26.0",
"@typescript-eslint/parser": "^5.26.0",
"eslint": "^8.16.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
"typescript": "^4.7.2"
},
"dependencies": {
"chalk": "^4.1.2",
"commander": "^9.2.0",
"leven": "^3.1.0",
"minimist": "^1.2.6",
"read-pkg": "^5.1.1",
"semver": "^7.3.7",
"sharp": "^0.30.5",
"strip-ansi": "^6.0.0"
}
}
================================================
FILE: tsconfig.json
================================================
{
"extends": "@tsconfig/recommended/tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"noImplicitAny": true,
"preserveConstEnums": true,
"sourceMap": true,
"resolveJsonModule": true,
"outDir": "dist"
},
"include": ["./**/*.ts"],
"exclude": ["node_modules", "**/*.spec.ts"],
}
================================================
FILE: utils/dir.ts
================================================
import fs from 'fs';
export default (dir: string) => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
};
================================================
FILE: utils/enhanceErrorMessages.ts
================================================
import { program } from 'commander';
import { chalk } from '.';
export default (methodName: string, log: (...args: any[]) => string) => {
(program as any)[methodName] = function enhanceErrorMessages(...args: any[]) {
/* eslint-disable no-underscore-dangle */
if (methodName === 'unknownOption' && this._allowUnknownOption) {
return;
}
this.outputHelp();
console.log(` ${chalk.red(log(...args))}`);
console.log();
process.exit(1);
};
};
================================================
FILE: utils/index.ts
================================================
import chalk from 'chalk';
import createDirectory from './dir';
import resolveConfig from './pkg';
export { chalk, createDirectory, resolveConfig };
export * as semver from 'semver';
export * from './logger';
================================================
FILE: utils/logger.ts
================================================
/* eslint-disable no-debugger, no-console */
import chalk from 'chalk';
import stripAnsi from 'strip-ansi';
const format = (label: string, msg: string) => msg.split('\n').map((line, i) => (
i === 0
? `${label} ${line}`
: line.padStart(stripAnsi(label).length + line.length + 1)
)).join('\n');
const chalkTag = (msg: string) => chalk.bgBlackBright.white.dim(` ${msg} `);
export const log = (msg = '', tag = null) => (
tag ? console.log(format(chalkTag(tag), msg)) : console.log(msg)
);
export const info = (msg: string, tag = null) => {
console.log(format(chalk.bgBlue.black(' INFO ') + (tag ? chalkTag(tag) : ''), msg));
};
export const warn = (msg: string, tag = null) => {
console.warn(format(chalk.bgYellow.black(' WARN ') + (tag ? chalkTag(tag) : ''), chalk.yellow(msg)));
};
export const error = (msg: string, tag = null) => {
console.error(format(chalk.bgRed(' ERROR ') + (tag ? chalkTag(tag) : ''), chalk.red(msg)));
};
================================================
FILE: utils/pkg.ts
================================================
import fs from 'fs';
import path from 'path';
import readPkg from 'read-pkg';
export default (context: string) => {
if (fs.existsSync(path.join(context, '.iconsetrc.js'))) {
return require(path.join(context, '.iconsetrc.js'));
}
if (fs.existsSync(path.join(context, '.iconsetrc.json'))) {
return require(path.join(context, '.iconsetrc.json'));
}
if (fs.existsSync(path.join(context, 'package.json'))) {
return readPkg.sync({ cwd: context })?.iconsetConfig || {};
}
return {};
};