Skip to content

Commit 1ebe984

Browse files
committed
feat(android): provide fallback for adaptive icons flow
resolves #31
1 parent 3763f2a commit 1ebe984

File tree

10 files changed

+248
-169
lines changed

10 files changed

+248
-169
lines changed

src/__tests__/cli.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe('cordova-res', () => {
1010
it('should provide defaults with no args', async () => {
1111
expect(generateRunOptions(Platform.ANDROID, 'resources', [])).toEqual({
1212
'adaptive-icon': {
13+
icon: { sources: ['resources/android/icon.png', 'resources/android/icon.jpg', 'resources/android/icon.jpeg', 'resources/icon.png', 'resources/icon.jpg', 'resources/icon.jpeg'] },
1314
foreground: { sources: ['resources/android/icon-foreground.png', 'resources/android/icon-foreground.jpg', 'resources/android/icon-foreground.jpeg'] },
1415
background: { sources: ['resources/android/icon-background.png', 'resources/android/icon-background.jpg', 'resources/android/icon-background.jpeg'] },
1516
},

src/__tests__/config.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import * as et from 'elementtree';
22

33
import { runResource } from '../config';
4-
import { ResourceKey, ResourceType } from '../resources';
54
import { GeneratedResource, Platform } from '../platform';
5+
import { ResourceKey, ResourceNodeAttributeType, ResourceType } from '../resources';
6+
7+
const SRC_ATTRIBUTE = { key: ResourceKey.SRC, type: ResourceNodeAttributeType.PATH };
68

79
describe('cordova-res', () => {
810

@@ -13,10 +15,10 @@ describe('cordova-res', () => {
1315
const resource: GeneratedResource = {
1416
[ResourceKey.SRC]: '/path/to/resources/icon.png',
1517
type: ResourceType.ICON,
16-
srckey: ResourceKey.SRC,
1718
platform: Platform.ANDROID,
1819
nodeName: 'icon',
19-
nodeAttributes: [ResourceKey.SRC],
20+
nodeAttributes: [SRC_ATTRIBUTE],
21+
indexAttribute: SRC_ATTRIBUTE,
2022
};
2123

2224
it('should insert node for empty container', async () => {

src/__tests__/image.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Platform } from '../platform';
12
import { ResourceType } from '../resources';
23

34
describe('cordova-res', () => {
@@ -31,20 +32,20 @@ describe('cordova-res', () => {
3132
});
3233

3334
it('should throw with empty array of source images', async () => {
34-
await expect(image.resolveSourceImage(ResourceType.ICON, [])).rejects.toThrow('Missing source image');
35+
await expect(image.resolveSourceImage(Platform.ANDROID, ResourceType.ICON, [])).rejects.toThrow('Missing source image');
3536
expect(fsMock.readFile).not.toHaveBeenCalled();
3637
});
3738

3839
it('should throw with source image with error', async () => {
3940
fsMock.readFile.mockImplementation(async () => { throw new Error('err'); });
40-
await expect(image.resolveSourceImage(ResourceType.ICON, ['blah.png'])).rejects.toThrow('Missing source image');
41+
await expect(image.resolveSourceImage(Platform.ANDROID, ResourceType.ICON, ['blah.png'])).rejects.toThrow('Missing source image');
4142
expect(fsMock.readFile).toHaveBeenCalledTimes(1);
4243
});
4344

4445
it('should resolve with proper image', async () => {
4546
fsMock.readFile.mockImplementationOnce(async () => { throw new Error('err'); });
4647
fsMock.readFile.mockImplementationOnce(async () => Buffer.from([]));
47-
const { src } = await image.resolveSourceImage(ResourceType.ICON, ['foo.png', 'bar.png']);
48+
const { src } = await image.resolveSourceImage(Platform.ANDROID, ResourceType.ICON, ['foo.png', 'bar.png']);
4849
expect(src).toEqual('bar.png');
4950
expect(fsMock.readFile).toHaveBeenCalledTimes(2);
5051
expect(resourcesMock.RASTER_RESOURCE_VALIDATORS[ResourceType.ICON]).toHaveBeenCalledTimes(1);

src/__tests__/platform.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe('cordova-res', () => {
4242
const generatedImages = getResourcesConfig(Platform.ANDROID, ResourceType.ICON).resources;
4343

4444
expect(imageMock.resolveSourceImage).toHaveBeenCalledTimes(1);
45-
expect(imageMock.resolveSourceImage).toHaveBeenCalledWith('icon', ['icon.png'], undefined);
45+
expect(imageMock.resolveSourceImage).toHaveBeenCalledWith('android', 'icon', ['icon.png'], undefined);
4646
expect(imageMock.generateImage).toHaveBeenCalledTimes(generatedImages.length);
4747

4848
for (const generatedImage of generatedImages) {

src/cli.ts

+60-59
Original file line numberDiff line numberDiff line change
@@ -28,91 +28,92 @@ export function generateRunOptions(platform: Platform, resourcesDirectory: strin
2828
const types = validateResourceTypes(typeOption ? [typeOption] : RESOURCE_TYPES);
2929

3030
return {
31-
[ResourceType.ADAPTIVE_ICON]: types.includes(ResourceType.ADAPTIVE_ICON) ? parseAdaptiveIconOptions(platform, resourcesDirectory, args) : undefined,
32-
[ResourceType.ICON]: types.includes(ResourceType.ICON) ? parseIconOptions(platform, resourcesDirectory, args) : undefined,
33-
[ResourceType.SPLASH]: types.includes(ResourceType.SPLASH) ? parseSplashOptions(platform, resourcesDirectory, args) : undefined,
31+
[ResourceType.ADAPTIVE_ICON]: types.includes(ResourceType.ADAPTIVE_ICON) ? parseAdaptiveIconResourceOptions(platform, resourcesDirectory, args) : undefined,
32+
[ResourceType.ICON]: types.includes(ResourceType.ICON) ? parseSimpleResourceOptions(platform, ResourceType.ICON, resourcesDirectory, args) : undefined,
33+
[ResourceType.SPLASH]: types.includes(ResourceType.SPLASH) ? parseSimpleResourceOptions(platform, ResourceType.SPLASH, resourcesDirectory, args) : undefined,
3434
};
3535
}
3636

37-
export function parseAdaptiveIconOptions(platform: Platform, resourcesDirectory: string, args: ReadonlyArray<string>): AdaptiveIconResourceOptions | undefined {
37+
export function parseAdaptiveIconResourceOptions(platform: Platform, resourcesDirectory: string, args: ReadonlyArray<string>): AdaptiveIconResourceOptions | undefined {
3838
if (platform !== Platform.ANDROID) {
3939
return;
4040
}
4141

4242
return {
43-
foreground: parseAdaptiveIconTypeOptions(ResourceKey.FOREGROUND, resourcesDirectory, args),
44-
background: parseAdaptiveIconTypeOptions(ResourceKey.BACKGROUND, resourcesDirectory, args),
43+
icon: parseSimpleResourceOptions(platform, ResourceType.ICON, resourcesDirectory, args),
44+
foreground: parseAdaptiveIconForegroundOptions(resourcesDirectory, args),
45+
background: parseAdaptiveIconBackgroundOptions(resourcesDirectory, args),
4546
};
4647
}
4748

48-
export function parseAdaptiveIconTypeOptions(type: ResourceKey.FOREGROUND | ResourceKey.BACKGROUND, resourcesDirectory: string, args: ReadonlyArray<string>): AdaptiveIconResourceOptions[typeof type] {
49-
const sourceOption = getOptionValue(args, `--icon-${type}-source`);
50-
const options: Partial<AdaptiveIconResourceOptions[typeof type]> = {};
49+
export function parseAdaptiveIconForegroundOptions(resourcesDirectory: string, args: ReadonlyArray<string>): AdaptiveIconResourceOptions['foreground'] {
50+
const source = parseAdaptiveIconSourceFromArgs(ResourceKey.FOREGROUND, args);
5151

52-
if (sourceOption) {
53-
const source: Source = sourceOption.startsWith('#')
54-
? { type: SourceType.COLOR, color: sourceOption }
55-
: { type: SourceType.RASTER, src: sourceOption };
52+
if (source && source.type !== SourceType.RASTER) {
53+
throw new BadInputError('Adaptive icon foreground must be an image.');
54+
}
5655

57-
if (type === ResourceKey.FOREGROUND && source.type !== SourceType.RASTER) {
58-
throw new BadInputError('Adaptive icon foreground must be an image.');
59-
}
56+
return {
57+
sources: source
58+
? [source]
59+
: getDefaultAdaptiveIconSources(ResourceKey.FOREGROUND, resourcesDirectory),
60+
};
61+
}
6062

61-
options.sources = [source];
62-
}
63+
export function parseAdaptiveIconBackgroundOptions(resourcesDirectory: string, args: ReadonlyArray<string>): AdaptiveIconResourceOptions['background'] {
64+
const source = parseAdaptiveIconSourceFromArgs(ResourceKey.BACKGROUND, args);
6365

6466
return {
65-
sources: [
66-
`${resourcesDirectory}/android/icon-${type}.png`,
67-
`${resourcesDirectory}/android/icon-${type}.jpg`,
68-
`${resourcesDirectory}/android/icon-${type}.jpeg`,
69-
],
70-
...options,
67+
sources: source
68+
? [source]
69+
: getDefaultAdaptiveIconSources(ResourceKey.BACKGROUND, resourcesDirectory),
7170
};
7271
}
7372

74-
export function parseIconOptions(platform: Platform, resourcesDirectory: string, args: ReadonlyArray<string>): SimpleResourceOptions {
75-
const sourceOption = getOptionValue(args, '--icon-source');
76-
const options: Partial<SimpleResourceOptions> = {};
73+
export function parseSimpleResourceOptions(platform: Platform, type: ResourceType.ICON | ResourceType.SPLASH, resourcesDirectory: string, args: ReadonlyArray<string>): SimpleResourceOptions {
74+
const source = parseSourceFromArgs(type, args);
75+
return { sources: source ? [source] : getDefaultSources(platform, type, resourcesDirectory) };
76+
}
7777

78-
if (sourceOption) {
79-
options.sources = [sourceOption];
78+
export function parseAdaptiveIconSourceFromArgs(type: ResourceKey.FOREGROUND | ResourceKey.BACKGROUND, args: ReadonlyArray<string>): Source | undefined {
79+
const sourceOption = getOptionValue(args, `--icon-${type}-source`);
80+
81+
if (!sourceOption) {
82+
return;
8083
}
8184

82-
return {
83-
...{
84-
sources: [
85-
`${resourcesDirectory}/${platform}/icon.png`,
86-
`${resourcesDirectory}/${platform}/icon.jpg`,
87-
`${resourcesDirectory}/${platform}/icon.jpeg`,
88-
`${resourcesDirectory}/icon.png`,
89-
`${resourcesDirectory}/icon.jpg`,
90-
`${resourcesDirectory}/icon.jpeg`,
91-
],
92-
},
93-
...options,
94-
};
85+
return parseSource(sourceOption);
9586
}
9687

97-
export function parseSplashOptions(platform: Platform, resourcesDirectory: string, args: ReadonlyArray<string>): SimpleResourceOptions {
98-
const sourceOption = getOptionValue(args, '--splash-source');
99-
const options: Partial<SimpleResourceOptions> = {};
88+
export function parseSourceFromArgs(type: ResourceType.ICON | ResourceType.SPLASH, args: ReadonlyArray<string>): string | undefined {
89+
const sourceOption = getOptionValue(args, `--${type}-source`);
10090

10191
if (sourceOption) {
102-
options.sources = [sourceOption];
92+
return sourceOption;
10393
}
94+
}
10495

105-
return {
106-
...{
107-
sources: [
108-
`${resourcesDirectory}/${platform}/splash.png`,
109-
`${resourcesDirectory}/${platform}/splash.jpg`,
110-
`${resourcesDirectory}/${platform}/splash.jpeg`,
111-
`${resourcesDirectory}/splash.png`,
112-
`${resourcesDirectory}/splash.jpg`,
113-
`${resourcesDirectory}/splash.jpeg`,
114-
],
115-
},
116-
...options,
117-
};
96+
export function parseSource(sourceOption: string): Source {
97+
return sourceOption.startsWith('#')
98+
? { type: SourceType.COLOR, color: sourceOption }
99+
: { type: SourceType.RASTER, src: sourceOption };
100+
}
101+
102+
export function getDefaultSources(platform: Platform, type: ResourceType, resourcesDirectory: string): string[] {
103+
return [
104+
`${resourcesDirectory}/${platform}/${type}.png`,
105+
`${resourcesDirectory}/${platform}/${type}.jpg`,
106+
`${resourcesDirectory}/${platform}/${type}.jpeg`,
107+
`${resourcesDirectory}/${type}.png`,
108+
`${resourcesDirectory}/${type}.jpg`,
109+
`${resourcesDirectory}/${type}.jpeg`,
110+
];
111+
}
112+
113+
export function getDefaultAdaptiveIconSources(type: ResourceKey.FOREGROUND | ResourceKey.BACKGROUND, resourcesDirectory: string): string[] {
114+
return [
115+
`${resourcesDirectory}/android/icon-${type}.png`,
116+
`${resourcesDirectory}/android/icon-${type}.jpg`,
117+
`${resourcesDirectory}/android/icon-${type}.jpeg`,
118+
];
118119
}

src/config.ts

+20-37
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import pathlib from 'path';
55

66
import { BadInputError } from './error';
77
import { GeneratedResource, Platform } from './platform';
8-
import { ResolvedColorSource, ResolvedSource, ResourceType, SourceType } from './resources';
8+
import { ResolvedColorSource, ResolvedSource, ResourceNodeAttribute, ResourceNodeAttributeType, SourceType } from './resources';
99

1010
const debug = Debug('cordova-res:config');
1111

@@ -31,24 +31,6 @@ export async function run(configPath: string, resourcesDirectory: string, source
3131
resourceFileElement.set('target', '/app/src/main/res/values/colors.xml');
3232
}
3333

34-
if (resources.find(resource => resource.type === ResourceType.ADAPTIVE_ICON)) {
35-
debug('Adaptive Icon resources found--removing any regular icon nodes.');
36-
37-
const regularIconElements = androidPlatformElement.findall('icon[@src]');
38-
39-
for (const element of regularIconElements) {
40-
androidPlatformElement.remove(element);
41-
}
42-
} else {
43-
debug('No Adaptive Icon resources found--removing any adaptive icon nodes.');
44-
45-
const regularIconElements = androidPlatformElement.findall('icon[@foreground]');
46-
47-
for (const element of regularIconElements) {
48-
androidPlatformElement.remove(element);
49-
}
50-
}
51-
5234
runConfig(configPath, resources, config);
5335

5436
await write(configPath, config);
@@ -102,27 +84,26 @@ export function runConfig(configPath: string, resources: ReadonlyArray<Generated
10284
}
10385
}
10486

87+
export function conformPath(configPath: string, value: string | number): string {
88+
return pathlib.relative(pathlib.dirname(configPath), value.toString()).replace(/\\/g, '/');
89+
}
90+
10591
export function runResource(configPath: string, resource: GeneratedResource, container: et.Element): void {
106-
const src = resource[resource.srckey];
92+
const src = resource[resource.indexAttribute.key];
10793

10894
if (typeof src !== 'string') {
109-
throw new BadInputError(`Bad value for src key: ${resource.srckey}`);
95+
throw new BadInputError(`Bad value for index "${resource.indexAttribute.key}": ${src}`);
11096
}
11197

11298
// We force the use of forward slashes here to provide cross-platform
11399
// compatibility for paths.
114-
const dest = pathlib.relative(pathlib.dirname(configPath), src).replace(/\\/g, '/');
115-
const imgElement = resolveResourceElement(container, resource.nodeName, resource.srckey, dest);
100+
const imgElement = resolveResourceElement(container, resource.nodeName, resource.indexAttribute, conformPath(configPath, src));
116101

117102
for (const attr of resource.nodeAttributes) {
118-
let v = resource[attr];
103+
const v = resource[attr.key];
119104

120105
if (v) {
121-
if (attr === resource.srckey) {
122-
v = dest;
123-
}
124-
125-
imgElement.set(attr, v.toString());
106+
imgElement.set(attr.key, attr.type === ResourceNodeAttributeType.PATH ? conformPath(configPath, v) : v.toString());
126107
}
127108
}
128109
}
@@ -138,22 +119,24 @@ export function resolvePlatformElement(container: et.Element, platform: Platform
138119
return et.SubElement(container, 'platform', { name: platform });
139120
}
140121

141-
export function resolveResourceElement(container: et.Element, nodeName: string, pathAttr: string, pathAttrValue: string): et.Element {
142-
const imgElement = container.find(`${nodeName}[@${pathAttr}='${pathAttrValue}']`);
122+
export function resolveResourceElement(container: et.Element, nodeName: string, indexAttr: ResourceNodeAttribute, index: string): et.Element {
123+
const imgElement = container.find(`${nodeName}[@${indexAttr.key}='${index}']`);
143124

144125
if (imgElement) {
145126
return imgElement;
146127
}
147128

148-
// We didn't find the element using forward slashes, so let's try to
149-
// find it with backslashes.
150-
const imgElementByBackslashes = container.find(`${nodeName}[@${pathAttr}='${pathAttrValue.replace(/\//g, '\\')}']`);
129+
if (indexAttr.type === ResourceNodeAttributeType.PATH) {
130+
// We didn't find the element using forward slashes, so let's try to
131+
// find it with backslashes if the index is a path.
132+
const imgElementByBackslashes = container.find(`${nodeName}[@${indexAttr.key}='${index.replace(/\//g, '\\')}']`);
151133

152-
if (imgElementByBackslashes) {
153-
return imgElementByBackslashes;
134+
if (imgElementByBackslashes) {
135+
return imgElementByBackslashes;
136+
}
154137
}
155138

156-
debug('Creating %O node for %o', nodeName, pathAttrValue);
139+
debug('Creating %O node for %o', nodeName, index);
157140
return et.SubElement(container, nodeName);
158141
}
159142

src/image.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,20 @@ import sharp, { Metadata, Sharp } from 'sharp';
44
import util from 'util';
55

66
import { ResolveSourceImageError, ValidationError } from './error';
7+
import { Platform } from './platform';
78
import { Format, RASTER_RESOURCE_VALIDATORS, ResolvedImageSource, ResourceType, SourceType } from './resources';
89

910
const debug = Debug('cordova-res:image');
1011

1112
/**
1213
* Check an array of source files, returning the first viable image.
1314
*/
14-
export async function resolveSourceImage(type: ResourceType, sources: string[], errstream?: NodeJS.WritableStream): Promise<ResolvedImageSource> {
15+
export async function resolveSourceImage(platform: Platform, type: ResourceType, sources: string[], errstream?: NodeJS.WritableStream): Promise<ResolvedImageSource> {
1516
const errors: [string, NodeJS.ErrnoException][] = [];
1617

1718
for (const source of sources) {
1819
try {
19-
return await readSourceImage(type, source, errstream);
20+
return await readSourceImage(platform, type, source, errstream);
2021
} catch (e) {
2122
errors.push([source, e]);
2223
}
@@ -32,13 +33,15 @@ export async function resolveSourceImage(type: ResourceType, sources: string[],
3233
);
3334
}
3435

35-
export async function readSourceImage(type: ResourceType, src: string, errstream?: NodeJS.WritableStream): Promise<ResolvedImageSource> {
36+
export async function readSourceImage(platform: Platform, type: ResourceType, src: string, errstream?: NodeJS.WritableStream): Promise<ResolvedImageSource> {
3637
const image = sharp(await readFile(src));
3738
const metadata = await RASTER_RESOURCE_VALIDATORS[type](src, image);
3839

3940
debug('Source image for %s: %O', type, metadata);
4041

4142
return {
43+
platform,
44+
resource: type,
4245
type: SourceType.RASTER,
4346
src,
4447
image: { src, pipeline: image, metadata },

src/index.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,14 @@ async function CordovaRes({
7171

7272
return {
7373
resources: resources.map(resource => {
74-
const { src, foreground, background, platform, width, height, density, orientation } = resource;
74+
const { platform, type, src, foreground, background, width, height, density, orientation } = resource;
7575

7676
return {
77+
platform,
78+
type,
7779
src,
7880
foreground,
7981
background,
80-
platform,
8182
width,
8283
height,
8384
density,
@@ -87,9 +88,9 @@ async function CordovaRes({
8788
sources: sources.map(source => {
8889
switch (source.type) {
8990
case SourceType.RASTER:
90-
return { type: SourceType.RASTER, value: source.src };
91+
return { platform: source.platform, resource: source.resource, type: SourceType.RASTER, value: source.src };
9192
case SourceType.COLOR:
92-
return { type: SourceType.COLOR, name: source.name, value: source.color };
93+
return { platform: source.platform, resource: source.resource, type: SourceType.COLOR, value: source.color, name: source.name };
9394
}
9495
}),
9596
};

0 commit comments

Comments
 (0)