From 625042695149efb2cc121c596ddd868b459cd4e7 Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Mon, 23 Mar 2020 16:39:46 -0700 Subject: [PATCH] feat: --skip-config option for skipping config.xml read/write Also clean up some docs and config structure. --- README.md | 26 +++++++--- src/__tests__/cli.ts | 113 ++++++++++++++++++++++++++++++++++++++++--- src/cli.ts | 43 +++++++++++----- src/help.ts | 7 +-- src/index.ts | 78 ++++++++++++++++------------- src/native.ts | 50 +++++++++++++------ 6 files changed, 243 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index d3b5828..2f84b52 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Resource Generator -This tool will crop and resize JPEG and PNG source images to generate images for modern iOS and Android devices. It will also register the generated images in `config.xml` so that Cordova projects are updated accordingly. +This tool will crop and resize JPEG and PNG source images to generate icons and splash screens for modern iOS, Android, and Windows. `cordova-res` was developed for use with Cordova, but Capacitor and other native runtimes are supported. ## Install @@ -10,7 +10,7 @@ $ npm install -g cordova-res ## Usage -`cordova-res` must run at the root of a Cordova project, such as: +`cordova-res` expects a Cordova project structure such as: ``` resources/ @@ -19,8 +19,9 @@ resources/ config.xml ``` -* `resources/icon.png` must be at least 1024×1024px -* `resources/splash.png` must be at least 2732×2732px +* `resources/icon.(png|jpg)` must be at least 1024×1024px +* `resources/splash.(png|jpg)` must be at least 2732×2732px +* `config.xml` is optional. If present, the generated images are registered accordingly To generate resources with all the default options, just run: @@ -34,7 +35,7 @@ $ cordova-res $ cordova-res ios ``` -Otherwise, `cordova-res` looks for platforms in `config.xml` (e.g. ``) and generates resources only for them. +Otherwise, if `config.xml` exists, `cordova-res` will look for platforms (e.g. ``) and generate resources only for the configured platforms. #### Documentation @@ -55,7 +56,20 @@ A color may also be used for the icon background by specifying the `--icon-backg Regular Android icons will still be generated as a fallback for Android devices that do not support adaptive icons. -:memo: **Note**: Cordova 9+ and `cordova-android` 8+ is required. +:memo: **Note**: For Cordova apps, Cordova 9+ and `cordova-android` 8+ is required. + +### Capacitor + +To use `cordova-res` in Capacitor and other native runtimes, it is recommended to use `--skip-config` (skips reading & writing to Cordova's `config.xml` file) and `--copy` (copies generated resources into native projects). + +For example, to generate icons and splash screens for iOS and Android in Capacitor, run: + +```bash +$ cordova-res ios --skip-config --copy +$ cordova-res android --skip-config --copy +``` + +You can use `--ios-project` and `--android-project` to specify the native project directories into which these resources are copied. By default, `cordova-res` copies Android resources into `android/` and iOS resources into `ios/` (the directories Capacitor uses). ### Tips diff --git a/src/__tests__/cli.ts b/src/__tests__/cli.ts index bf7d4d5..c81d3f8 100644 --- a/src/__tests__/cli.ts +++ b/src/__tests__/cli.ts @@ -2,6 +2,103 @@ import { Options } from '..'; import { generateRunOptions, parseOptions } from '../cli'; import { Platform } from '../platform'; +function generatePlatformsConfig(resourcesDirectory: string) { + return { + android: { + 'adaptive-icon': { + background: { + sources: [ + `${resourcesDirectory}/android/icon-background.png`, + `${resourcesDirectory}/android/icon-background.jpg`, + `${resourcesDirectory}/android/icon-background.jpeg`, + ], + }, + foreground: { + sources: [ + `${resourcesDirectory}/android/icon-foreground.png`, + `${resourcesDirectory}/android/icon-foreground.jpg`, + `${resourcesDirectory}/android/icon-foreground.jpeg`, + ], + }, + icon: { + sources: [ + `${resourcesDirectory}/android/icon.png`, + `${resourcesDirectory}/android/icon.jpg`, + `${resourcesDirectory}/android/icon.jpeg`, + `${resourcesDirectory}/icon.png`, + `${resourcesDirectory}/icon.jpg`, + `${resourcesDirectory}/icon.jpeg`, + ], + }, + }, + icon: { + sources: [ + `${resourcesDirectory}/android/icon.png`, + `${resourcesDirectory}/android/icon.jpg`, + `${resourcesDirectory}/android/icon.jpeg`, + `${resourcesDirectory}/icon.png`, + `${resourcesDirectory}/icon.jpg`, + `${resourcesDirectory}/icon.jpeg`, + ], + }, + splash: { + sources: [ + `${resourcesDirectory}/android/splash.png`, + `${resourcesDirectory}/android/splash.jpg`, + `${resourcesDirectory}/android/splash.jpeg`, + `${resourcesDirectory}/splash.png`, + `${resourcesDirectory}/splash.jpg`, + `${resourcesDirectory}/splash.jpeg`, + ], + }, + }, + ios: { + icon: { + sources: [ + `${resourcesDirectory}/ios/icon.png`, + `${resourcesDirectory}/ios/icon.jpg`, + `${resourcesDirectory}/ios/icon.jpeg`, + `${resourcesDirectory}/icon.png`, + `${resourcesDirectory}/icon.jpg`, + `${resourcesDirectory}/icon.jpeg`, + ], + }, + splash: { + sources: [ + `${resourcesDirectory}/ios/splash.png`, + `${resourcesDirectory}/ios/splash.jpg`, + `${resourcesDirectory}/ios/splash.jpeg`, + `${resourcesDirectory}/splash.png`, + `${resourcesDirectory}/splash.jpg`, + `${resourcesDirectory}/splash.jpeg`, + ], + }, + }, + windows: { + icon: { + sources: [ + `${resourcesDirectory}/windows/icon.png`, + `${resourcesDirectory}/windows/icon.jpg`, + `${resourcesDirectory}/windows/icon.jpeg`, + `${resourcesDirectory}/icon.png`, + `${resourcesDirectory}/icon.jpg`, + `${resourcesDirectory}/icon.jpeg`, + ], + }, + splash: { + sources: [ + `${resourcesDirectory}/windows/splash.png`, + `${resourcesDirectory}/windows/splash.jpg`, + `${resourcesDirectory}/windows/splash.jpeg`, + `${resourcesDirectory}/splash.png`, + `${resourcesDirectory}/splash.jpg`, + `${resourcesDirectory}/splash.jpeg`, + ], + }, + }, + }; +} + describe('cordova-res', () => { describe('cli', () => { @@ -9,14 +106,18 @@ describe('cordova-res', () => { describe('parseOptions', () => { const DEFAULT_OPTIONS: Options = { + directory: process.cwd(), logstream: process.stdout, errstream: process.stderr, resourcesDirectory: 'resources', - nativeProject: { - enabled: false, - androidProjectDirectory: '', - iosProjectDirectory: '', + platforms: generatePlatformsConfig('resources'), + projectConfig: { + android: { directory: 'android' }, + ios: { directory: 'ios' }, + windows: { directory: 'windows' }, }, + skipConfig: false, + copy: false, }; it('should parse default options with no arguments', () => { @@ -27,7 +128,7 @@ describe('cordova-res', () => { it('should parse options for android', () => { const args = ['android']; const result = parseOptions(args); - expect(result).toEqual({ ...DEFAULT_OPTIONS, platforms: { android: generateRunOptions(Platform.ANDROID, 'resources', args) } }); + expect(result).toEqual({ ...DEFAULT_OPTIONS, platforms: { android: generateRunOptions(Platform.ANDROID, 'resources', args), }, projectConfig: { android: { directory: 'android' } } }); }); it('should parse default options when the first argument is not a platform', () => { @@ -39,7 +140,7 @@ describe('cordova-res', () => { it('should accept --resources flag', () => { const args = ['--resources', 'res']; const result = parseOptions(args); - expect(result).toEqual({ ...DEFAULT_OPTIONS, resourcesDirectory: 'res' }); + expect(result).toEqual({ ...DEFAULT_OPTIONS, platforms: generatePlatformsConfig('res'), resourcesDirectory: 'res' }); }); it('should log to stderr with --json flag', () => { diff --git a/src/cli.ts b/src/cli.ts index 7969d20..586ccdb 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,10 +1,10 @@ import et from 'elementtree'; -import { Options, PlatformOptions } from '.'; +import type { NativeProjectConfigByPlatform, Options, PlatformOptions } from '.'; import { getPlatforms } from './config'; import { BadInputError } from './error'; -import { NativeProject } from './native'; -import { AdaptiveIconResourceOptions, Platform, RunPlatformOptions, SimpleResourceOptions, filterSupportedPlatforms, validatePlatforms } from './platform'; +import { NativeProjectConfig } from './native'; +import { AdaptiveIconResourceOptions, PLATFORMS, Platform, RunPlatformOptions, SimpleResourceOptions, filterSupportedPlatforms, validatePlatforms } from './platform'; import { DEFAULT_RESOURCES_DIRECTORY, RESOURCE_TYPES, ResourceKey, ResourceType, Source, SourceType, validateResourceTypes } from './resources'; import { getOptionValue } from './utils/cli'; @@ -24,23 +24,21 @@ export async function resolveOptions(args: readonly string[], directory: string, }; } -export function parseOptions(args: readonly string[]): Options { +export function parseOptions(args: readonly string[]): Required { const json = args.includes('--json'); const resourcesDirectory = getOptionValue(args, '--resources', DEFAULT_RESOURCES_DIRECTORY); const platformArg = args[0] ? args[0] : undefined; - const platformList = validatePlatforms(platformArg && !platformArg.startsWith('-') ? [platformArg] : []); - const nativeProject: Readonly = { - enabled: args.includes('--copy'), - androidProjectDirectory: getOptionValue(args, '--android-project', ''), - iosProjectDirectory: getOptionValue(args, '--ios-project', ''), - }; + const platformList = validatePlatforms(platformArg && !platformArg.startsWith('-') ? [platformArg] : PLATFORMS); return { + directory: getDirectory(), resourcesDirectory, logstream: json ? process.stderr : process.stdout, errstream: process.stderr, - ...platformList.length > 0 ? { platforms: generatePlatformOptions(platformList, resourcesDirectory, args) } : {}, - nativeProject, + platforms: generatePlatformOptions(platformList, resourcesDirectory, args), + projectConfig: generatePlatformProjectOptions(platformList, args), + skipConfig: parseSkipConfigOption(args), + copy: parseCopyOption(args), }; } @@ -51,6 +49,13 @@ export function generatePlatformOptions(platforms: readonly Platform[], resource }, {} as PlatformOptions); } +export function generatePlatformProjectOptions(platforms: readonly Platform[], args: readonly string[]): NativeProjectConfigByPlatform { + return platforms.reduce((acc, platform) => { + acc[platform] = generateNativeProjectConfig(platform, args); + return acc; + }, {} as NativeProjectConfigByPlatform); +} + export function generateRunOptions(platform: Platform, resourcesDirectory: string, args: readonly string[]): RunPlatformOptions { const typeOption = getOptionValue(args, '--type'); const types = validateResourceTypes(typeOption ? [typeOption] : RESOURCE_TYPES); @@ -62,6 +67,20 @@ export function generateRunOptions(platform: Platform, resourcesDirectory: strin }; } +export function generateNativeProjectConfig(platform: Platform, args: readonly string[]): NativeProjectConfig { + const directory = getOptionValue(args, `--${platform}-project`, platform); + + return { directory }; +} + +export function parseCopyOption(args: readonly string[]): boolean { + return args.includes('--copy'); +} + +export function parseSkipConfigOption(args: readonly string[]): boolean { + return args.includes('--skip-config'); +} + export function parseAdaptiveIconResourceOptions(platform: Platform, resourcesDirectory: string, args: readonly string[]): AdaptiveIconResourceOptions | undefined { if (platform !== Platform.ANDROID) { return; diff --git a/src/help.ts b/src/help.ts index 74c7bdb..ce649f0 100644 --- a/src/help.ts +++ b/src/help.ts @@ -1,5 +1,5 @@ const help = ` - Usage: cordova-res [ios|android] [options] + Usage: cordova-res [ios|android|windows] [options] Generate Cordova resources for native platforms. @@ -28,15 +28,16 @@ const help = ` --type ... Only generate one type of resource --resources ................... Use specified directory as resources directory + --skip-config ........................ Skip reading/writing to 'config.xml' --icon-source ................. Use specified file for icon source image --splash-source ............... Use specified file for splash source image --icon-foreground-source ...... Use file for foreground of adaptive icon --icon-background-source .. Use file or color for background of adaptive icon - --copy ............................... Enable process of copying generated resources to native projects - --android-project ............. Use specified directory for Android native project (default: 'android') + --copy ............................... Copy generated resources to native projects --ios-project ................. Use specified directory for iOS native project (default: 'ios') + --android-project ............. Use specified directory for Android native project (default: 'android') -h, --help ........................... Print help for the platform, then quit --version ............................ Print version, then quit diff --git a/src/index.ts b/src/index.ts index 8782c89..32d125e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,12 +3,12 @@ import Debug from 'debug'; import et from 'elementtree'; import path from 'path'; -import { generateRunOptions, getDirectory, resolveOptions } from './cli'; +import { getDirectory, parseOptions, resolveOptions } from './cli'; import { getConfigPath, read as readConfig, run as runConfig, write as writeConfig } from './config'; import { BaseError } from './error'; -import { NativeProject, copyToNativeProject } from './native'; +import { NativeProjectConfig, copyToNativeProject } from './native'; import { GeneratedResource, PLATFORMS, Platform, RunPlatformOptions, run as runPlatform } from './platform'; -import { DEFAULT_RESOURCES_DIRECTORY, Density, Orientation, ResolvedSource, SourceType } from './resources'; +import { Density, Orientation, ResolvedSource, SourceType } from './resources'; import { tryFn } from './utils/fn'; const debug = Debug('cordova-res'); @@ -34,22 +34,19 @@ interface ResultSource { value: string; } -async function CordovaRes({ - directory = getDirectory(), - resourcesDirectory = DEFAULT_RESOURCES_DIRECTORY, - logstream = process.stdout, - errstream = process.stderr, - platforms = { - [Platform.ANDROID]: generateRunOptions(Platform.ANDROID, resourcesDirectory, []), - [Platform.IOS]: generateRunOptions(Platform.IOS, resourcesDirectory, []), - [Platform.WINDOWS]: generateRunOptions(Platform.WINDOWS, resourcesDirectory, []), - }, - nativeProject = { - enabled: false, - androidProjectDirectory: '', - iosProjectDirectory: '', - }, -}: CordovaRes.Options = {}): Promise { +async function CordovaRes(options: CordovaRes.Options = {}): Promise { + const defaultOptions = parseOptions([]); + const { + directory, + resourcesDirectory, + logstream, + errstream, + platforms, + projectConfig, + skipConfig, + copy, + } = { ...defaultOptions, ...options }; + const configPath = getConfigPath(directory); debug('Paths: (config: %O) (resources dir: %O)', configPath, resourcesDirectory); @@ -58,26 +55,32 @@ async function CordovaRes({ const resources: GeneratedResource[] = []; const sources: ResolvedSource[] = []; - if (await pathWritable(configPath)) { - config = await readConfig(configPath); - } else { - debug('File missing/not writable: %O', configPath); + if (!skipConfig) { + if (await pathWritable(configPath)) { + config = await readConfig(configPath); + } else { + debug('File missing/not writable: %O', configPath); - if (errstream) { - errstream.write(`WARN: No config.xml file in directory. Skipping config.\n`); + if (errstream) { + errstream.write(`WARN: No config.xml file in directory. Skipping config.\n`); + } } } for (const platform of PLATFORMS) { const platformOptions = platforms[platform]; + const nativeProject = projectConfig[platform]; if (platformOptions) { const platformResult = await runPlatform(platform, resourcesDirectory, platformOptions, errstream); + logstream.write(`Generated ${platformResult.resources.length} resources for ${platform}\n`); + resources.push(...platformResult.resources); sources.push(...platformResult.sources); - if (nativeProject.enabled) { - await copyToNativeProject(platform, nativeProject, logstream); + + if (copy && nativeProject) { + await copyToNativeProject(platform, nativeProject, logstream, errstream); } } } @@ -117,10 +120,11 @@ async function CordovaRes({ } namespace CordovaRes { - export type PlatformOptions = { [P in Platform]?: Readonly; }; - export const run = CordovaRes; + export type PlatformOptions = { [P in Platform]?: Readonly; }; + export type NativeProjectConfigByPlatform = { [P in Platform]?: Readonly; }; + /** * Options for `cordova-res`. * @@ -167,11 +171,19 @@ namespace CordovaRes { readonly platforms?: Readonly; /** - * Specify target native project directory. - * - * This is for copying all generated resources directly. + * Config for the native projects by platform. + */ + readonly projectConfig?: Readonly; + + /** + * Do not use the Cordova config.xml file. + */ + readonly skipConfig?: boolean; + + /** + * Copy generated resources to native project directories. */ - readonly nativeProject?: Readonly; + readonly copy?: boolean; } export async function runCommandLine(args: readonly string[]): Promise { diff --git a/src/native.ts b/src/native.ts index de4359e..122df25 100644 --- a/src/native.ts +++ b/src/native.ts @@ -4,11 +4,10 @@ import path from 'path'; import { Platform } from './platform'; -export interface NativeProject { - enabled?: boolean; - androidProjectDirectory?: string; - iosProjectDirectory?: string; +export interface NativeProjectConfig { + directory: string; } + interface ProcessItem { source: string; target: string; @@ -28,6 +27,9 @@ const SOURCE_ANDROID_SPLASH = 'resources/android/splash/'; const TARGET_ANDROID_ICON = '/app/src/main/res/'; const TARGET_ANDROID_SPLASH = '/app/src/main/res/'; +// TODO: IOS_ICONS, IOS_SPLASHES, ANDROID_ICONS, and ANDROID_SPLASHES should +// probably be part of RESOURCES config. + const IOS_ICONS: readonly ProcessItem[] = [ { source: 'icon-20.png', target: 'AppIcon-20x20@1x.png' }, { source: 'icon-20@2x.png', target: 'AppIcon-20x20@2x.png' }, @@ -48,6 +50,7 @@ const IOS_ICONS: readonly ProcessItem[] = [ { source: 'icon-83.5@2x.png', target: 'AppIcon-83.5x83.5@2x.png' }, { source: 'icon-1024.png', target: 'AppIcon-512@2x.png' }, ]; + const IOS_SPLASHES: readonly ProcessItem[] = [ { source: 'Default-Portrait@~ipadpro.png', target: 'splash-2732x2732.png' }, { source: 'Default-Portrait@~ipadpro.png', target: 'splash-2732x2732-1.png' }, @@ -55,8 +58,14 @@ const IOS_SPLASHES: readonly ProcessItem[] = [ ]; const ANDROID_ICONS: readonly ProcessItem[] = [ - { source: 'drawable-ldpi-icon.png', target: 'drawable-hdpi-icon.png' }, - { source: 'drawable-mdpi-icon.png', target: 'mipmap-mdpi/ic_launcher.png' }, + { + source: 'drawable-ldpi-icon.png', + target: 'drawable-hdpi-icon.png', + }, + { + source: 'drawable-mdpi-icon.png', + target: 'mipmap-mdpi/ic_launcher.png', + }, { source: 'drawable-mdpi-icon.png', target: 'mipmap-mdpi/ic_launcher_round.png', @@ -65,7 +74,10 @@ const ANDROID_ICONS: readonly ProcessItem[] = [ source: 'drawable-mdpi-icon.png', target: 'mipmap-mdpi/ic_launcher_foreground.png', }, - { source: 'drawable-hdpi-icon.png', target: 'mipmap-hdpi/ic_launcher.png' }, + { + source: 'drawable-hdpi-icon.png', + target: 'mipmap-hdpi/ic_launcher.png', + }, { source: 'drawable-hdpi-icon.png', target: 'mipmap-hdpi/ic_launcher_round.png', @@ -74,7 +86,10 @@ const ANDROID_ICONS: readonly ProcessItem[] = [ source: 'drawable-hdpi-icon.png', target: 'mipmap-hdpi/ic_launcher_foreground.png', }, - { source: 'drawable-xhdpi-icon.png', target: 'mipmap-xhdpi/ic_launcher.png' }, + { + source: 'drawable-xhdpi-icon.png', + target: 'mipmap-xhdpi/ic_launcher.png', + }, { source: 'drawable-xhdpi-icon.png', target: 'mipmap-xhdpi/ic_launcher_round.png', @@ -108,6 +123,7 @@ const ANDROID_ICONS: readonly ProcessItem[] = [ target: 'mipmap-xxxhdpi/ic_launcher_foreground.png', }, ]; + const ANDROID_SPLASHES: readonly ProcessItem[] = [ { source: 'drawable-land-mdpi-screen.png', target: 'drawable/splash.png' }, { @@ -156,21 +172,27 @@ async function copyImages(sourcePath: string, targetPath: string, images: readon await Promise.all(images.map(async item => { const source = path.join(sourcePath, item.source); const target = path.join(targetPath, item.target); + + debug('Copying generated resource from %O to %O', source, target); + await copy(source, target); - debug('Copied resource item from %s to %s', source, target); })); } -export async function copyToNativeProject(platform: Platform, nativeProject: NativeProject, logstream: NodeJS.WritableStream) { +export async function copyToNativeProject(platform: Platform, nativeProject: NativeProjectConfig, logstream: NodeJS.WritableStream, errstream?: NodeJS.WritableStream) { if (platform === Platform.IOS) { - const iosProjectDirectory = nativeProject.iosProjectDirectory || 'ios'; + const iosProjectDirectory = nativeProject.directory || 'ios'; await copyImages(SOURCE_IOS_ICON, path.join(iosProjectDirectory, TARGET_IOS_ICON), IOS_ICONS); await copyImages(SOURCE_IOS_SPLASH, path.join(iosProjectDirectory, TARGET_IOS_SPLASH), IOS_SPLASHES); - logstream.write(`Copied ${IOS_ICONS.length + IOS_SPLASHES.length} resource items to iOS project`); + logstream.write(`Copied ${IOS_ICONS.length + IOS_SPLASHES.length} resource items to ${platform}\n`); } else if (platform === Platform.ANDROID) { - const androidProjectDirectory = nativeProject.androidProjectDirectory || 'android'; + const androidProjectDirectory = nativeProject.directory || 'android'; await copyImages(SOURCE_ANDROID_ICON, path.join(androidProjectDirectory, TARGET_ANDROID_ICON), ANDROID_ICONS); await copyImages(SOURCE_ANDROID_SPLASH, path.join(androidProjectDirectory, TARGET_ANDROID_SPLASH), ANDROID_SPLASHES); - logstream.write(`Copied ${ANDROID_ICONS.length + ANDROID_SPLASHES.length} resource items to Android project`); + logstream.write(`Copied ${ANDROID_ICONS.length + ANDROID_SPLASHES.length} resource items to ${platform}\n`); + } else { + if (errstream) { + errstream.write(`WARN: Copying to native projects is not supported for ${platform}\n`); + } } }