From d77cc10f1c46f27ec6911c14ce20d5a5401ce069 Mon Sep 17 00:00:00 2001 From: Lawrence Hunt Date: Fri, 26 Apr 2019 16:24:57 +1000 Subject: [PATCH 1/8] chore: Migrate to async/await BREAKING CHANGE: Support for Node 6 deprecated, now minimum Node 8 --- package-lock.json | 32 +++----- package.json | 3 + src/android/find-android-manifests.js | 2 +- src/android/find-android-manifests.specs.js | 36 ++++----- .../generate-manifest-adaptive-icons.js | 15 ++-- .../generate-manifest-adaptive-icons.specs.js | 18 ++--- src/android/generate-manifest-icons.js | 29 ++++---- src/android/generate-manifest-icons.specs.js | 18 ++--- src/generate.js | 71 +++++++++--------- src/generate.specs.js | 28 ++++--- src/imagemagick/get-image-width.js | 20 +++-- src/imagemagick/get-image-width.specs.js | 15 ++-- src/init/init.js | 28 ++++--- src/init/init.specs.js | 20 ++--- src/ios/find-iconset-folders.js | 2 +- src/ios/find-iconset-folders.specs.js | 43 +++++------ src/ios/generate-iconset-icons.js | 73 +++++++++---------- src/ios/generate-iconset-icons.specs.js | 54 ++++++-------- src/label/label-image.js | 42 +++++------ src/label/label-image.specs.js | 50 +++++-------- src/resize/resize-image.js | 2 +- src/resize/resize-image.specs.js | 26 +++---- 22 files changed, 277 insertions(+), 350 deletions(-) diff --git a/package-lock.json b/package-lock.json index 244ed7f..c9d5cdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -267,8 +267,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -329,7 +328,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -593,8 +591,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -1808,8 +1805,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "function-bind": { "version": "1.1.1", @@ -2051,7 +2047,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2254,7 +2249,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -2263,8 +2257,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", @@ -3020,7 +3013,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3486,7 +3478,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -3648,8 +3639,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -3951,12 +3941,11 @@ "dev": true }, "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "run-async": { @@ -4890,8 +4879,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "0.2.1", diff --git a/package.json b/package.json index b77f813..c7d2fae 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "0.11.0", "description": "Automatic icon resizing for Mobile Apps. Supports Native, Cordova and React Native. Inspired by cordova-icon.", "main": "./src/index.js", + "engines": { + "node": ">=8" + }, "bin": { "app-icon": "./bin/app-icon.js" }, diff --git a/src/android/find-android-manifests.js b/src/android/find-android-manifests.js index 45c8acc..6df93a1 100644 --- a/src/android/find-android-manifests.js +++ b/src/android/find-android-manifests.js @@ -7,7 +7,7 @@ const rexNodeModules = new RegExp('node_modules'); const rexBuildFoler = new RegExp(`${path.normalize('/build/').replace(/\\/g, '\\\\')}`); // Given a search root, finds all iOS iconsets. -module.exports = function findAndroidManifests(searchRoot) { +module.exports = async function findAndroidManifests(searchRoot) { return find(searchRoot, (file, stat) => { // Exclude: node modules and android build intermediates. if (file.match(rexNodeModules)) return false; diff --git a/src/android/find-android-manifests.specs.js b/src/android/find-android-manifests.specs.js index b395c5a..35b3e42 100644 --- a/src/android/find-android-manifests.specs.js +++ b/src/android/find-android-manifests.specs.js @@ -3,31 +3,27 @@ const path = require('path'); const findAndroidManifests = require('./find-android-manifests'); describe('find-android-manifests', () => { - it('should not find any manifests in the node_modules/ folder', () => { - return findAndroidManifests('./node_modules').then((manifests) => { - expect(manifests.length).to.equal(0); - }); + it('should not find any manifests in the node_modules/ folder', async () => { + const manifests = await findAndroidManifests('./node_modules'); + expect(manifests.length).to.equal(0); }); - it('should be able to find the React Native manifest', () => { - return findAndroidManifests('./test/ReactNativeIconTest').then((manifests) => { - expect(manifests.length).to.equal(1); - expect(manifests).to.include(path.normalize('test/ReactNativeIconTest/android/app/src/main/AndroidManifest.xml')); - }); + it('should be able to find the React Native manifest', async () => { + const manifests = await findAndroidManifests('./test/ReactNativeIconTest'); + expect(manifests.length).to.equal(1); + expect(manifests).to.include(path.normalize('test/ReactNativeIconTest/android/app/src/main/AndroidManifest.xml')); }); - it('should be able to find the Cordova manifests', () => { - return findAndroidManifests('./test/CordovaApp').then((manifests) => { - expect(manifests.length).to.equal(2); - expect(manifests).to.include(path.normalize('test/CordovaApp/platforms/android/AndroidManifest.xml')); - expect(manifests).to.include(path.normalize('test/CordovaApp/platforms/android/CordovaLib/AndroidManifest.xml')); - }); + it('should be able to find the Cordova manifests', async () => { + const manifests = await findAndroidManifests('./test/CordovaApp'); + expect(manifests.length).to.equal(2); + expect(manifests).to.include(path.normalize('test/CordovaApp/platforms/android/AndroidManifest.xml')); + expect(manifests).to.include(path.normalize('test/CordovaApp/platforms/android/CordovaLib/AndroidManifest.xml')); }); - it('should be able to find the Native manifest', () => { - return findAndroidManifests('./test/NativeApp').then((manifests) => { - expect(manifests.length).to.equal(1); - expect(manifests).to.include(path.normalize('test/NativeApp/android/native_app/src/main/AndroidManifest.xml')); - }); + it('should be able to find the Native manifest', async () => { + const manifests = await findAndroidManifests('./test/NativeApp'); + expect(manifests.length).to.equal(1); + expect(manifests).to.include(path.normalize('test/NativeApp/android/native_app/src/main/AndroidManifest.xml')); }); }); diff --git a/src/android/generate-manifest-adaptive-icons.js b/src/android/generate-manifest-adaptive-icons.js index 4b1dc81..cd84252 100644 --- a/src/android/generate-manifest-adaptive-icons.js +++ b/src/android/generate-manifest-adaptive-icons.js @@ -16,7 +16,7 @@ const icLauncherManifestXml = + `${EOL}`; // Generate Android Manifest icons given a manifest file. -module.exports = function generateManifestIcons(backgroundIcon, foregroundIcon, manifest) { +module.exports = async function generateManifestIcons(backgroundIcon, foregroundIcon, manifest) { // Create the object we will return. const results = { icons: [], @@ -26,7 +26,7 @@ module.exports = function generateManifestIcons(backgroundIcon, foregroundIcon, const manifestFolder = path.dirname(manifest); // Generate each image in the full icon set, updating the contents. - return Promise.all(androidManifestAdaptiveIcons.adaptiveIcons.map((icon) => { + await Promise.all(androidManifestAdaptiveIcons.adaptiveIcons.map(async (icon) => { // Each icon lives in its own folder, so we'd better make sure that folder // exists. return new Promise((resolve, reject) => { @@ -55,10 +55,9 @@ module.exports = function generateManifestIcons(backgroundIcon, foregroundIcon, return resolve(Promise.all(operations)); }); }); - })).then(() => { - // Before writing the contents file, sort the contents (otherwise - // they could be in a different order each time). - results.icons.sort(); - return results; - }); + })); + // Before writing the contents file, sort the contents (otherwise + // they could be in a different order each time). + results.icons.sort(); + return results; }; diff --git a/src/android/generate-manifest-adaptive-icons.specs.js b/src/android/generate-manifest-adaptive-icons.specs.js index ab20500..72d9efa 100644 --- a/src/android/generate-manifest-adaptive-icons.specs.js +++ b/src/android/generate-manifest-adaptive-icons.specs.js @@ -41,7 +41,7 @@ const testManifests = [{ describe('generate-manifest-adaptive-icons', () => { // Run each test. testManifests.forEach(({ projectName, manifestPath }) => { - it(`should be able to generate adaptive icons for the ${projectName} manifest`, () => { + it(`should be able to generate adaptive icons for the ${projectName} manifest`, async () => { // Get the manifest folder, create an array of every icon we expect to see. const manifestFolder = path.dirname(manifestPath); const resourceFolders = expectedFolders.map(f => path.join(manifestFolder, f)); @@ -58,16 +58,12 @@ describe('generate-manifest-adaptive-icons', () => { expectedPaths.forEach(f => console.log(`Expecting: ${f}`)); // Delete all of the folders we're expecting to create, then generate the icons. - return Promise.all(resourceFolders.map(deleteFolderIfExists)) - .then(() => ( - generateManifestAdaptiveIcons(backgroundIcon, foregroundIcon, manifestPath) - )) - .then(() => Promise.all(expectedPaths.map(fileExists))) - .then((filesDoExist) => { - filesDoExist.forEach((exists, index) => { - expect(exists, `${resourceFoldersFiles[index]} should be generated`).to.equal(true); - }); - }); + await Promise.all(resourceFolders.map(deleteFolderIfExists)); + await (generateManifestAdaptiveIcons(backgroundIcon, foregroundIcon, manifestPath)); + const filesDoExist = await Promise.all(expectedPaths.map(fileExists)); + filesDoExist.forEach((exists, index) => { + expect(exists, `${resourceFoldersFiles[index]} should be generated`).to.equal(true); + }); }); }); }); diff --git a/src/android/generate-manifest-icons.js b/src/android/generate-manifest-icons.js index c0e2fb5..65fb9b9 100644 --- a/src/android/generate-manifest-icons.js +++ b/src/android/generate-manifest-icons.js @@ -1,11 +1,14 @@ const path = require('path'); const mkdirp = require('mkdirp'); +const { promisify } = require('util'); + +const mkdirpAsync = promisify(mkdirp); const androidManifestIcons = require('./AndroidManifest.icons.json'); const resizeImage = require('../resize/resize-image'); // Generate Android Manifest icons given a manifest file. -module.exports = function generateManifestIcons(sourceIcon, manifest) { +module.exports = async function generateManifestIcons(sourceIcon, manifest) { // Create the object we will return. const results = { icons: [], @@ -15,22 +18,18 @@ module.exports = function generateManifestIcons(sourceIcon, manifest) { const manifestFolder = path.dirname(manifest); // Generate each image in the full icon set, updating the contents. - return Promise.all(androidManifestIcons.icons.map((icon) => { + await Promise.all(androidManifestIcons.icons.map(async (icon) => { const targetPath = path.join(manifestFolder, icon.path); // Each icon lives in its own folder, so we'd better make sure that folder // exists. - return new Promise((resolve, reject) => { - mkdirp(path.dirname(targetPath), (err) => { - if (err) return reject(err); - results.icons.push(icon.path); - return resolve(resizeImage(sourceIcon, targetPath, icon.size)); - }); - }); - })).then(() => { - // Before writing the contents file, sort the contents (otherwise - // they could be in a different order each time). - results.icons.sort(); - return results; - }); + await mkdirpAsync(path.dirname(targetPath)); + results.icons.push(icon.path); + + return resizeImage(sourceIcon, targetPath, icon.size); + })); + // Before writing the contents file, sort the contents (otherwise + // they could be in a different order each time). + results.icons.sort(); + return results; }; diff --git a/src/android/generate-manifest-icons.specs.js b/src/android/generate-manifest-icons.specs.js index 455e1bb..a49be38 100644 --- a/src/android/generate-manifest-icons.specs.js +++ b/src/android/generate-manifest-icons.specs.js @@ -36,7 +36,7 @@ const testManifests = [{ describe('generate-manifest-icons', () => { // Run each test. testManifests.forEach(({ projectName, manifestPath }) => { - it(`should be able to generate icons and round icons for the ${projectName} manifest`, () => { + it(`should be able to generate icons and round icons for the ${projectName} manifest`, async () => { // Get the manifest folder, create an array of every icon we expect to see. const manifestFolder = path.dirname(manifestPath); const resourceFolders = expectedFolders.map(f => path.join(manifestFolder, f)); @@ -46,16 +46,12 @@ describe('generate-manifest-icons', () => { }, []); // Delete all of the folders we're expecting to create, then generate the icons. - return Promise.all(resourceFolders.map(deleteFolderIfExists)) - .then(() => ( - generateManifestIcons(sourceIcon, manifestPath) - )) - .then(() => Promise.all(resourceFoldersFiles.map(fileExists))) - .then((filesDoExist) => { - filesDoExist.forEach((exists, index) => { - expect(exists, `${resourceFoldersFiles[index]} should be generated`).to.equal(true); - }); - }); + await Promise.all(resourceFolders.map(deleteFolderIfExists)); + await (generateManifestIcons(sourceIcon, manifestPath)); + const filesDoExist = await Promise.all(resourceFoldersFiles.map(fileExists)); + filesDoExist.forEach((exists, index) => { + expect(exists, `${resourceFoldersFiles[index]} should be generated`).to.equal(true); + }); }); }); }); diff --git a/src/generate.js b/src/generate.js index a652ff8..33d3cec 100644 --- a/src/generate.js +++ b/src/generate.js @@ -6,7 +6,7 @@ const generateManifestIcons = require('./android/generate-manifest-icons'); const generateManifestAdaptiveIcons = require('./android/generate-manifest-adaptive-icons'); const validateParameters = require('./validate-parameters'); -module.exports = function generate(parameters) { +module.exports = async function generate(parameters) { // Validate and coerce the parameters. const { sourceIcon, @@ -24,45 +24,42 @@ module.exports = function generate(parameters) { adaptiveIconManifests: [], }; - return findIconsetFolders(searchRoot) - .then(iconSets => Promise.all(iconSets.map((iconset) => { - if (!platforms.includes('ios')) return null; + const iconSets = await findIconsetFolders(searchRoot); + await Promise.all(iconSets.map(async (iconset) => { + if (!platforms.includes('ios')) return null; + console.log(`Found iOS iconset: ${iconset}...`); - console.log(`Found iOS iconset: ${iconset}...`); + const { icons } = await generateIconsetIcons(sourceIcon, iconset); + results.iconsets.push({ iconset, icons }); + icons.forEach((icon) => { + console.log(` ${chalk.green('✓')} Generated icon ${icon}`); + }); + console.log(` ${chalk.green('✓')} Updated Contents.json`); - return generateIconsetIcons(sourceIcon, iconset) - .then(({ icons }) => { - results.iconsets.push({ iconset, icons }); - icons.forEach((icon) => { - console.log(` ${chalk.green('✓')} Generated icon ${icon}`); - }); - console.log(` ${chalk.green('✓')} Updated Contents.json`); + return null; + })); + const manifests = await findAndroidManifests(searchRoot); + await Promise.all(manifests.map(async (manifest) => { + if (!platforms.includes('android')) return null; + console.log(`Found Android Manifest: ${manifest}...`); + const operations = [ + generateManifestIcons(sourceIcon, manifest).then(({ icons }) => { + results.manifests.push({ manifest, icons }); + icons.forEach((icon) => { + console.log(` ${chalk.green('✓')} Generated icon ${icon}`); }); - }))) - .then(() => findAndroidManifests(searchRoot)) - .then(manifests => Promise.all(manifests.map((manifest) => { - if (!platforms.includes('android')) return null; - - console.log(`Found Android Manifest: ${manifest}...`); - const operations = [ - generateManifestIcons(sourceIcon, manifest).then(({ icons }) => { - results.manifests.push({ manifest, icons }); + }), + ]; + if (adaptiveIcons) { + operations.push(generateManifestAdaptiveIcons(backgroundIcon, foregroundIcon, manifest) + .then(({ icons }) => { + results.adaptiveIconManifests.push({ manifest, icons }); icons.forEach((icon) => { - console.log(` ${chalk.green('✓')} Generated icon ${icon}`); + console.log(` ${chalk.green('✓')} Generated adaptive icon ${icon}`); }); - }), - ]; - - if (adaptiveIcons) { - operations.push(generateManifestAdaptiveIcons(backgroundIcon, foregroundIcon, manifest) - .then(({ icons }) => { - results.adaptiveIconManifests.push({ manifest, icons }); - icons.forEach((icon) => { - console.log(` ${chalk.green('✓')} Generated adaptive icon ${icon}`); - }); - })); - } - return Promise.all(operations); - }))) - .then(() => results); + })); + } + return Promise.all(operations); + })); + return results; }; diff --git a/src/generate.specs.js b/src/generate.specs.js index 286b7f5..d8e9ff6 100644 --- a/src/generate.specs.js +++ b/src/generate.specs.js @@ -2,22 +2,21 @@ const { expect } = require('chai'); const generate = require('./generate'); describe('generate', () => { - it('should be able to generate test app icons', () => { + it('should be able to generate test app icons', async () => { const parameters = { sourceIcon: './test/icon.png', searchPath: './', }; // Delete all of the files we're expecting to create, then generate them. - return generate(parameters).then((results) => { - // TODO: Check we found the manifests etc etc - expect(results).not.to.equal(null); - expect(results.iconsets.length).to.equal(3); - expect(results.manifests.length).to.equal(4); - }); + const results = await generate(parameters); + // TODO: Check we found the manifests etc etc + expect(results).not.to.equal(null); + expect(results.iconsets.length).to.equal(3); + expect(results.manifests.length).to.equal(4); }); - it('should be able to generate test app icons with adaptive icons included', () => { + it('should be able to generate test app icons with adaptive icons included', async () => { const parameters = { sourceIcon: './test/icon.png', backgroundIcon: './test/icon.background.png', @@ -27,12 +26,11 @@ describe('generate', () => { }; // Delete all of the files we're expecting to create, then generate them. - return generate(parameters).then((results) => { - // TODO: Check we found the manifests etc etc - expect(results).not.to.equal(null); - expect(results.iconsets.length).to.equal(3); - expect(results.manifests.length).to.equal(4); - expect(results.adaptiveIconManifests.length).to.equal(4); - }); + const results = await generate(parameters); + // TODO: Check we found the manifests etc etc + expect(results).not.to.equal(null); + expect(results.iconsets.length).to.equal(3); + expect(results.manifests.length).to.equal(4); + expect(results.adaptiveIconManifests.length).to.equal(4); }); }); diff --git a/src/imagemagick/get-image-width.js b/src/imagemagick/get-image-width.js index 6d5e585..5aaf6fa 100644 --- a/src/imagemagick/get-image-width.js +++ b/src/imagemagick/get-image-width.js @@ -1,15 +1,13 @@ const imagemagickCli = require('imagemagick-cli'); -module.exports = function getImageWidth(path) { - return imagemagickCli.exec(`identify -format %w "${path}"`) - .then(({ stdout }) => { - // Attempt to turn the width into pixels. - const pixelWidth = parseInt(stdout, 10); - if (Number.isNaN(pixelWidth)) { - console.log(`returned width '${stdout}' cannot be parsed into a number`); - return Promise.reject(new Error(`Cannot parse returned width '${stdout}'`)); - } +module.exports = async function getImageWidth(path) { + const { stdout } = await imagemagickCli.exec(`identify -format %w "${path}"`); + // Attempt to turn the width into pixels. + const pixelWidth = parseInt(stdout, 10); + if (Number.isNaN(pixelWidth)) { + console.log(`returned width '${stdout}' cannot be parsed into a number`); + throw new Error(`Cannot parse returned width '${stdout}'`); + } - return Promise.resolve(pixelWidth); - }); + return pixelWidth; }; diff --git a/src/imagemagick/get-image-width.specs.js b/src/imagemagick/get-image-width.specs.js index 24cbb71..1cccec4 100644 --- a/src/imagemagick/get-image-width.specs.js +++ b/src/imagemagick/get-image-width.specs.js @@ -2,15 +2,16 @@ const { expect } = require('chai'); const getImageWidth = require('./get-image-width'); describe('get-image-width', () => { - it('should be able to get the width of a test image', () => { - return getImageWidth('./src/label/test-images/input.png').then((width) => { - expect(width).to.equal(512); - }); + it('should be able to get the width of a test image', async () => { + const width = await getImageWidth('./src/label/test-images/input.png'); + expect(width).to.equal(512); }); - it('should reject with an error if the input is not an image', () => { - return getImageWidth('./src/label/get-image-width.js').catch((err) => { + it('should reject with an error if the input is not an image', async () => { + try { + await getImageWidth('./src/label/get-image-width.js'); + } catch (err) { expect(err).to.not.equal(null); - }); + } }); }); diff --git a/src/init/init.js b/src/init/init.js index 647a4bf..75d031e 100644 --- a/src/init/init.js +++ b/src/init/init.js @@ -10,28 +10,26 @@ const copyFile = require('../utils/copy-file'); * @param options * @returns a promise which resolves when the icon has been created. */ -function init(template, output, options) { +async function init(template, output, options) { // If there is no caption, then we can just copy the image. const caption = (options && options.caption) || ''; if (!caption) return copyFile(template, output); // We have a caption, so we'll need to get the image width to work out how // to arrange it on the icon. - return getImageWidth(template) - .then((width) => { - // The height will be 80% of the width, i.e. the text almost fills the icon. - // TODO: getImageWidth should be getImageDimensions - const w = Math.floor(width * 0.8); - const h = Math.floor(width * 0.8); + const width = await getImageWidth(template); + // The height will be 80% of the width, i.e. the text almost fills the icon. + // TODO: getImageWidth should be getImageDimensions + const w = Math.floor(width * 0.8); + const h = Math.floor(width * 0.8); - // Create the command to generate the image. - const command = `convert \ - -background "rgba(0,0,0,0)" -fill white \ - -gravity center -size ${w}x${h} \ - -stroke black -strokewidth 2 caption:"${caption}" \ - ${template} +swap -composite ${output}`; - return imagemagickCli.exec(command); - }); + // Create the command to generate the image. + const command = `convert \ + -background "rgba(0,0,0,0)" -fill white \ + -gravity center -size ${w}x${h} \ + -stroke black -strokewidth 2 caption:"${caption}" \ + ${template} +swap -composite ${output}`; + return imagemagickCli.exec(command); } module.exports = init; diff --git a/src/init/init.specs.js b/src/init/init.specs.js index cf8f931..74e0863 100644 --- a/src/init/init.specs.js +++ b/src/init/init.specs.js @@ -3,25 +3,21 @@ const init = require('./init'); const compareImages = require('../testing/compare-images'); describe('init', () => { - it('should be able to init an icon', () => { + it('should be able to init an icon', async () => { const template = './src/init/icon.template.png'; const output = './src/init/test-images/init-output.png'; const reference = './src/init/test-images/init-reference.png'; - return init(template, output) - .then(() => compareImages(output, reference)) - .then((difference) => { - expect(difference).to.be.below(5, 'Generated image is below accepted similarly threshold'); - }); + await init(template, output); + const difference = await compareImages(output, reference); + expect(difference).to.be.below(5, 'Generated image is below accepted similarly threshold'); }); - it('should be able to init an icon with a caption', () => { + it('should be able to init an icon with a caption', async () => { const template = './src/init/icon.template.png'; const output = './src/init/test-images/init-with-caption-output.png'; const reference = './src/init/test-images/init-with-caption-reference.png'; - return init(template, output, { caption: 'Test' }) - .then(() => compareImages(output, reference)) - .then((difference) => { - expect(difference).to.be.below(5, 'Generated image is below accepted similarly threshold'); - }); + await init(template, output, { caption: 'Test' }); + const difference = await compareImages(output, reference); + expect(difference).to.be.below(5, 'Generated image is below accepted similarly threshold'); }); }); diff --git a/src/ios/find-iconset-folders.js b/src/ios/find-iconset-folders.js index 8b9c58f..fa3b06d 100644 --- a/src/ios/find-iconset-folders.js +++ b/src/ios/find-iconset-folders.js @@ -1,7 +1,7 @@ const find = require('../utils/find'); // Given a search root, finds all iOS iconsets. -module.exports = function findIconsetFolders(searchRoot) { +module.exports = async function findIconsetFolders(searchRoot) { return find(searchRoot, (file, stat) => { // exclude node modules from the search. if (file.match(/node_modules/)) return false; diff --git a/src/ios/find-iconset-folders.specs.js b/src/ios/find-iconset-folders.specs.js index 7d103e6..c2a2678 100644 --- a/src/ios/find-iconset-folders.specs.js +++ b/src/ios/find-iconset-folders.specs.js @@ -3,37 +3,32 @@ const path = require('path'); const findIconsetFolders = require('./find-iconset-folders'); describe('find-iconset-folders', () => { - it('should not find any iconsets in the node_modules/ folder', () => { - return findIconsetFolders('./node_modules').then((iconsets) => { - expect(iconsets.length).to.equal(0); - }); + it('should not find any iconsets in the node_modules/ folder', async () => { + const iconsets = await findIconsetFolders('./node_modules'); + expect(iconsets.length).to.equal(0); }); - it('should be able to find the React Native iconset', () => { - return findIconsetFolders('./test/ReactNativeIconTest').then((iconsets) => { - expect(iconsets.length).to.equal(1); - expect(iconsets).to.include(path.normalize('test/ReactNativeIconTest/ios/ReactNativeIconTest/Images.xcassets/AppIcon.appiconset')); - }); + it('should be able to find the React Native iconset', async () => { + const iconsets = await findIconsetFolders('./test/ReactNativeIconTest'); + expect(iconsets.length).to.equal(1); + expect(iconsets).to.include(path.normalize('test/ReactNativeIconTest/ios/ReactNativeIconTest/Images.xcassets/AppIcon.appiconset')); }); - it('should be able to find the React Native iconset with a deep search path', () => { - return findIconsetFolders('./test/ReactNativeIconTest/ios/ReactNativeIconTest/Images.xcassets').then((iconsets) => { - expect(iconsets.length).to.equal(1); - expect(iconsets).to.include(path.normalize('test/ReactNativeIconTest/ios/ReactNativeIconTest/Images.xcassets/AppIcon.appiconset')); - }); + it('should be able to find the React Native iconset with a deep search path', async () => { + const iconsets = await findIconsetFolders('./test/ReactNativeIconTest/ios/ReactNativeIconTest/Images.xcassets'); + expect(iconsets.length).to.equal(1); + expect(iconsets).to.include(path.normalize('test/ReactNativeIconTest/ios/ReactNativeIconTest/Images.xcassets/AppIcon.appiconset')); }); - it('should be able to find the Cordova iconset', () => { - return findIconsetFolders('./test/CordovaApp').then((iconsets) => { - expect(iconsets.length).to.equal(1); - expect(iconsets).to.include(path.normalize('test/CordovaApp/platforms/ios/ionic_app/Images.xcassets/AppIcon.appiconset')); - }); + it('should be able to find the Cordova iconset', async () => { + const iconsets = await findIconsetFolders('./test/CordovaApp'); + expect(iconsets.length).to.equal(1); + expect(iconsets).to.include(path.normalize('test/CordovaApp/platforms/ios/ionic_app/Images.xcassets/AppIcon.appiconset')); }); - it('should be able to find the Native iconset', () => { - return findIconsetFolders('./test/NativeApp').then((iconsets) => { - expect(iconsets.length).to.equal(1); - expect(iconsets).to.include(path.normalize('test/NativeApp/ios/native_app/Assets.xcassets/AppIcon.appiconset')); - }); + it('should be able to find the Native iconset', async () => { + const iconsets = await findIconsetFolders('./test/NativeApp'); + expect(iconsets.length).to.equal(1); + expect(iconsets).to.include(path.normalize('test/NativeApp/ios/native_app/Assets.xcassets/AppIcon.appiconset')); }); }); diff --git a/src/ios/generate-iconset-icons.js b/src/ios/generate-iconset-icons.js index 27aa580..5212896 100644 --- a/src/ios/generate-iconset-icons.js +++ b/src/ios/generate-iconset-icons.js @@ -1,49 +1,44 @@ const path = require('path'); const fs = require('fs'); +const { promisify } = require('util'); + +const writeFileAsync = promisify(fs.writeFile); const resizeImage = require('../resize/resize-image'); const contentsTemplate = require('./AppIcon.iconset.Contents.template.json'); // Generate xCode icons given an iconset folder. -module.exports = function generateIconSetIcons(sourceIcon, iconset) { - return new Promise((resolve, reject) => { - // Build the results object. - const results = { - icons: [], - contentsPath: null, - }; +module.exports = async function generateIconSetIcons(sourceIcon, iconset) { + // Build the results object. + const results = { + icons: [], + contentsPath: null, + }; + + // We've got the iconset folder. Get the contents Json. + const contentsPath = path.join(iconset, 'Contents.json'); + const contents = JSON.parse(fs.readFileSync(contentsPath, 'utf8')); + contents.images = []; + + // Generate each image in the full icon set, updating the contents. + await Promise.all(contentsTemplate.images.map(async (image) => { + const targetName = `${image.idiom}-${image.size}-${image.scale}.png`; + const targetPath = path.join(iconset, targetName); + const targetScale = parseInt(image.scale.slice(0, 1), 10); + const targetSize = image.size.split('x').map(p => p * targetScale).join('x'); + await resizeImage(sourceIcon, targetPath, targetSize); + results.icons.push(targetName); + contents.images.push({ + size: image.size, + idiom: image.idiom, + scale: image.scale, + filename: targetName, + }); + })); - // We've got the iconset folder. Get the contents Json. - const contentsPath = path.join(iconset, 'Contents.json'); - const contents = JSON.parse(fs.readFileSync(contentsPath, 'utf8')); - contents.images = []; + contents.images.sort((imageA, imageB) => imageA.filename.localeCompare(imageB.filename)); + await writeFileAsync(contentsPath, JSON.stringify(contents, null, 2)); + results.contentsPath = contentsPath; - // Generate each image in the full icon set, updating the contents. - return Promise.all(contentsTemplate.images.map((image) => { - const targetName = `${image.idiom}-${image.size}-${image.scale}.png`; - const targetPath = path.join(iconset, targetName); - const targetScale = parseInt(image.scale.slice(0, 1), 10); - const targetSize = image.size.split('x').map(p => p * targetScale).join('x'); - return resizeImage(sourceIcon, targetPath, targetSize) - .then(() => { - results.icons.push(targetName); - contents.images.push({ - size: image.size, - idiom: image.idiom, - scale: image.scale, - filename: targetName, - }); - }); - })) - .then(() => { - // Before writing the contents file, sort the contents (otherwise - // they could be in a different order each time). - contents.images.sort((imageA, imageB) => imageA.filename.localeCompare(imageB.filename)); - fs.writeFile(contentsPath, JSON.stringify(contents, null, 2), (err) => { - if (err) return reject(err); - results.contentsPath = contentsPath; - return resolve(results); - }); - }); - }); + return results; }; diff --git a/src/ios/generate-iconset-icons.specs.js b/src/ios/generate-iconset-icons.specs.js index d4be730..6f15056 100644 --- a/src/ios/generate-iconset-icons.specs.js +++ b/src/ios/generate-iconset-icons.specs.js @@ -6,7 +6,7 @@ const fileExists = require('../utils/file-exists'); const sourceIcon = './test/icon.png'; describe('generate-iconset-icons', () => { - it('should be able to generate icons for the React Native iconset', () => { + it('should be able to generate icons for the React Native iconset', async () => { const files = [ 'test/ReactNativeIconTest/ios/ReactNativeIconTest/Images.xcassets/AppIcon.appiconset/ipad-40x40-1x.png', 'test/ReactNativeIconTest/ios/ReactNativeIconTest/Images.xcassets/AppIcon.appiconset/ipad-29x29-2x.png', @@ -28,19 +28,15 @@ describe('generate-iconset-icons', () => { ]; // Delete all of the files we're expecting to create, then generate them. - return Promise.all(files.map(deleteIfExists)) - .then(() => ( - generateIconsetIcons(sourceIcon, 'test/ReactNativeIconTest/ios/ReactNativeIconTest/Images.xcassets/AppIcon.appiconset') - )) - .then(() => Promise.all(files.map(fileExists))) - .then((filesDoExist) => { - filesDoExist.forEach((exists, index) => { - expect(exists, `${files[index]} should be generated`).to.equal(true); - }); - }); + await Promise.all(files.map(deleteIfExists)); + await (generateIconsetIcons(sourceIcon, 'test/ReactNativeIconTest/ios/ReactNativeIconTest/Images.xcassets/AppIcon.appiconset')); + const filesDoExist = await Promise.all(files.map(fileExists)); + filesDoExist.forEach((exists, index) => { + expect(exists, `${files[index]} should be generated`).to.equal(true); + }); }); - it('should be able to generate icons for the Cordova iconset', () => { + it('should be able to generate icons for the Cordova iconset', async () => { const files = [ 'test/CordovaApp/platforms/ios/ionic_app/Images.xcassets/AppIcon.appiconset/ipad-40x40-1x.png', 'test/CordovaApp/platforms/ios/ionic_app/Images.xcassets/AppIcon.appiconset/ipad-29x29-2x.png', @@ -62,19 +58,15 @@ describe('generate-iconset-icons', () => { ]; // Delete all of the files we're expecting to create, then generate them. - return Promise.all(files.map(deleteIfExists)) - .then(() => ( - generateIconsetIcons(sourceIcon, 'test/CordovaApp/platforms/ios/ionic_app/Images.xcassets/AppIcon.appiconset') - )) - .then(() => Promise.all(files.map(fileExists))) - .then((filesDoExist) => { - filesDoExist.forEach((exists, index) => { - expect(exists, `${files[index]} should be generated`).to.equal(true); - }); - }); + await Promise.all(files.map(deleteIfExists)); + await (generateIconsetIcons(sourceIcon, 'test/CordovaApp/platforms/ios/ionic_app/Images.xcassets/AppIcon.appiconset')); + const filesDoExist = await Promise.all(files.map(fileExists)); + filesDoExist.forEach((exists, index) => { + expect(exists, `${files[index]} should be generated`).to.equal(true); + }); }); - it('should be able to generate icons for the Native iconset', () => { + it('should be able to generate icons for the Native iconset', async () => { const files = [ 'test/NativeApp/ios/native_app/Assets.xcassets/AppIcon.appiconset/ipad-40x40-1x.png', 'test/NativeApp/ios/native_app/Assets.xcassets/AppIcon.appiconset/ipad-29x29-2x.png', @@ -96,15 +88,11 @@ describe('generate-iconset-icons', () => { ]; // Delete all of the files we're expecting to create, then generate them. - return Promise.all(files.map(deleteIfExists)) - .then(() => ( - generateIconsetIcons(sourceIcon, 'test/NativeApp/ios/native_app/Assets.xcassets/AppIcon.appiconset') - )) - .then(() => Promise.all(files.map(fileExists))) - .then((filesDoExist) => { - filesDoExist.forEach((exists, index) => { - expect(exists, `${files[index]} should be generated`).to.equal(true); - }); - }); + await Promise.all(files.map(deleteIfExists)); + await (generateIconsetIcons(sourceIcon, 'test/NativeApp/ios/native_app/Assets.xcassets/AppIcon.appiconset')); + const filesDoExist = await Promise.all(files.map(fileExists)); + filesDoExist.forEach((exists, index) => { + expect(exists, `${files[index]} should be generated`).to.equal(true); + }); }); }); diff --git a/src/label/label-image.js b/src/label/label-image.js index 71e93ee..11b54c5 100644 --- a/src/label/label-image.js +++ b/src/label/label-image.js @@ -14,35 +14,31 @@ const copyFile = require('../utils/copy-file'); * up by the caption. e.g. 0.2 means 20% of the icon will have the caption. * @returns a promise which resolves with the result of the CLI command. */ -function caption(input, output, label, gravity, proportionalSize) { +async function caption(input, output, label, gravity, proportionalSize) { // Get the image width. - return getImageWidth(input) - .then((width) => { - // The height is a proportional amount of the of the width. This means - // with a square image, a proportionalSize of 0.2 and a gravity of 'bottom', - // the bottom 20% of the icon will contain the caption. - const height = width * proportionalSize; + const width = await getImageWidth(input); + // The height is a proportional amount of the of the width. This means + // with a square image, a proportionalSize of 0.2 and a gravity of 'bottom', + // the bottom 20% of the icon will contain the caption. + const height = width * proportionalSize; - // Important: the quotes around the colour must be double quoutes for the - // command to work on Windows! - const command = `convert \ - -background "rgba(0,0,0,0.5)" -fill white \ - -gravity center -size ${width}x${height} \ - caption:"${label}" \ - ${input} +swap -gravity ${gravity} -composite ${output}`; - return imagemagickCli.exec(command); - }); + // Important: the quotes around the colour must be double quoutes for the + // command to work on Windows! + const command = `convert \ + -background "rgba(0,0,0,0.5)" -fill white \ + -gravity center -size ${width}x${height} \ + caption:"${label}" \ + ${input} +swap -gravity ${gravity} -composite ${output}`; + return imagemagickCli.exec(command); } // Single function to label an image (optionally top and bottom). -module.exports = function labelImage(input, output, top, bottom, middle) { +module.exports = async function labelImage(input, output, top, bottom, middle) { // First, create the output image. This will overwrite any existing file. - let promise = copyFile(input, output); + await copyFile(input, output); // We'll have a set of promises which we will run, which will update the image. - if (top) promise = promise.then(() => caption(output, output, top, 'north', 0.2)); - if (bottom) promise = promise.then(() => caption(output, output, bottom, 'south', 0.2)); - if (middle) promise = promise.then(() => caption(output, output, middle, 'center', 0.6)); - - return promise; + if (top) await caption(output, output, top, 'north', 0.2); + if (bottom) await caption(output, output, bottom, 'south', 0.2); + if (middle) await caption(output, output, middle, 'center', 0.6); }; diff --git a/src/label/label-image.specs.js b/src/label/label-image.specs.js index 4a877bf..13e721b 100644 --- a/src/label/label-image.specs.js +++ b/src/label/label-image.specs.js @@ -3,58 +3,48 @@ const labelImage = require('./label-image'); const compareImages = require('../testing/compare-images'); describe('labelImage', () => { - it('should be able to add a label to the top of an image', () => { + it('should be able to add a label to the top of an image', async () => { const input = './src/label/test-images/input.png'; const output = './src/label/test-images/label-top-output.png'; const reference = './src/label/test-images/label-top-reference.png'; - return labelImage(input, output, 'UAT', null) - .then(() => compareImages(output, reference)) - .then((difference) => { - expect(difference).to.be.below(10, 'Generated image is below accepted similarly threshold'); - }); + await labelImage(input, output, 'UAT', null); + const difference = await compareImages(output, reference); + expect(difference).to.be.below(10, 'Generated image is below accepted similarly threshold'); }); - it('should be able to add a label to the bottom of an image', () => { + it('should be able to add a label to the bottom of an image', async () => { const input = './src/label/test-images/input.png'; const output = './src/label/test-images/label-bottom-output.png'; const reference = './src/label/test-images/label-bottom-reference.png'; - return labelImage(input, output, null, '1.2.3') - .then(() => compareImages(output, reference)) - .then((difference) => { - expect(difference).to.be.below(10, 'Generated image is below accepted similarly threshold'); - }); + await labelImage(input, output, null, '1.2.3'); + const difference = await compareImages(output, reference); + expect(difference).to.be.below(10, 'Generated image is below accepted similarly threshold'); }); - it('should be able to add a label to the top and bottom of an image', () => { + it('should be able to add a label to the top and bottom of an image', async () => { const input = './src/label/test-images/input.png'; const output = './src/label/test-images/label-top-and-bottom-output.png'; const reference = './src/label/test-images/label-top-and-bottom-reference.png'; - return labelImage(input, output, 'top', 'bottom') - .then(() => compareImages(output, reference)) - .then((difference) => { - expect(difference).to.be.below(10, 'Generated image is below accepted similarly threshold'); - }); + await labelImage(input, output, 'top', 'bottom'); + const difference = await compareImages(output, reference); + expect(difference).to.be.below(10, 'Generated image is below accepted similarly threshold'); }); - it('should be able to add a label to the top and bottom and middle of an image', () => { + it('should be able to add a label to the top and bottom and middle of an image', async () => { const input = './src/label/test-images/input.png'; const output = './src/label/test-images/label-top-and-bottom-and-middle-output.png'; const reference = './src/label/test-images/label-top-and-bottom-and-middle-reference.png'; - return labelImage(input, output, 'top', 'bottom', 'middle') - .then(() => compareImages(output, reference)) - .then((difference) => { - expect(difference).to.be.below(10, 'Generated image is below accepted similarly threshold'); - }); + await labelImage(input, output, 'top', 'bottom', 'middle'); + const difference = await compareImages(output, reference); + expect(difference).to.be.below(10, 'Generated image is below accepted similarly threshold'); }); - it('should not label an image at all if no captions are specified', () => { + it('should not label an image at all if no captions are specified', async () => { const input = './src/label/test-images/input.png'; const output = './src/label/test-images/input-output.png'; const reference = './src/label/test-images/input.png'; - return labelImage(input, output, null, null, null) - .then(() => compareImages(output, reference)) - .then((difference) => { - expect(difference).to.be.below(1, 'Generated image is below accepted similarly threshold'); - }); + await labelImage(input, output, null, null, null); + const difference = await compareImages(output, reference); + expect(difference).to.be.below(1, 'Generated image is below accepted similarly threshold'); }); }); diff --git a/src/resize/resize-image.js b/src/resize/resize-image.js index 16f938a..824efda 100644 --- a/src/resize/resize-image.js +++ b/src/resize/resize-image.js @@ -1,6 +1,6 @@ const imagemagickCli = require('imagemagick-cli'); // Takes a source image, resizes to a target path. -module.exports = function resizeImage(source, target, size) { +module.exports = async function resizeImage(source, target, size) { return imagemagickCli.exec(`convert "${source}" -resize ${size} "${target}"`); }; diff --git a/src/resize/resize-image.specs.js b/src/resize/resize-image.specs.js index 25d7225..3b5b2f7 100644 --- a/src/resize/resize-image.specs.js +++ b/src/resize/resize-image.specs.js @@ -3,25 +3,23 @@ const resizeImage = require('./resize-image'); const compareImages = require('../testing/compare-images'); describe('resize-image', () => { - it('should be able to downscale an image', () => { + it('should be able to downscale an image', async () => { const input = './src/resize/test-images/input.png'; const output = './src/resize/test-images/output.png'; const reference = './src/resize/test-images/reference.png'; - return resizeImage(input, output, '16x16') - .then(() => { - return compareImages(output, reference); - }) - .then((difference) => { - // Note that locally, I can pass this test with a threshhold of 1. But - // on circle, it only manages about 18. Probably due to differing - // imagemagick versions... - expect(difference).to.be.below(20, 'Generated image is below accepted similarly threshold'); - }); + await resizeImage(input, output, '16x16'); + const difference = await compareImages(output, reference); + // Note that locally, I can pass this test with a threshhold of 1. But + // on circle, it only manages about 18. Probably due to differing + // imagemagick versions... + expect(difference).to.be.below(20, 'Generated image is below accepted similarly threshold'); }); - it('should fail with a sensible error message if imagemagick returns an error', () => { - return resizeImage('badinput', 'badoutput', 'badsize').catch((err) => { + it('should fail with a sensible error message if imagemagick returns an error', async () => { + try { + await resizeImage('badinput', 'badoutput', 'badsize'); + } catch (err) { expect(err.message).to.match(/failed/); - }); + } }); }); From 06ee78c1952a222ff8654aecad0ddf6cfa5abdb2 Mon Sep 17 00:00:00 2001 From: Lawrence Hunt Date: Fri, 26 Apr 2019 16:35:40 +1000 Subject: [PATCH 2/8] chore: Updated documentation to reference node 8 --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4de4fb5..35d502a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![npm version](https://badge.fury.io/js/app-icon.svg)](https://badge.fury.io/js/app-icon) [![CircleCI](https://circleci.com/gh/dwmkerr/app-icon.svg?style=shield)](https://circleci.com/gh/dwmkerr/app-icon) [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/3e334rknhjbpx555?svg=true)](https://ci.appveyor.com/project/dwmkerr/app-icon) [![codecov](https://codecov.io/gh/dwmkerr/app-icon/branch/master/graph/badge.svg)](https://codecov.io/gh/dwmkerr/app-icon) [![dependencies Status](https://david-dm.org/dwmkerr/app-icon/status.svg)](https://david-dm.org/dwmkerr/app-icon) [![devDependencies Status](https://david-dm.org/dwmkerr/app-icon/dev-status.svg)](https://david-dm.org/dwmkerr/app-icon?type=dev) [![GuardRails badge](https://badges.production.guardrails.io/dwmkerr/app-icon.svg)](https://www.guardrails.io) [![Greenkeeper badge](https://badges.greenkeeper.io/dwmkerr/app-icon.svg)](https://greenkeeper.io/) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) -Icon management for Mobile Apps. Create icons, generate all required sizes, label and annotate. Supports Native, Cordova, React Native, Xamarin and more. Inspired by [cordova-icon](https://github.com/AlexDisler/cordova-icon). Node 6 and onwards supported. +Icon management for Mobile Apps. Create icons, generate all required sizes, label and annotate. Supports Native, Cordova, React Native, Xamarin and more. Inspired by [cordova-icon](https://github.com/AlexDisler/cordova-icon). Node 8 and onwards supported. Banner @@ -181,7 +181,7 @@ Note that Adaptive Icons of *all* supported sizes are generated. However, we als ## Developer Guide -The only dependencies are Node 6 (or above) and Yarn. +The only dependencies are Node 8 (or above) and Yarn. Useful commands for development are: @@ -198,8 +198,8 @@ Currently the linting style is based on [airbnb](https://github.com/airbnb/javas Install the dependencies (I recommend [Node Version Manager](https://github.com/creationix/nvm)): ```bash -nvm install 6 -nvm use 6 +nvm install 8 +nvm use 8 git clone git@github.com:dwmkerr/app-icon.git cd app-icon npm install && npm test From 6b25ab777bb22ec3a84b4fa717c32313ff8a2ba4 Mon Sep 17 00:00:00 2001 From: Lawrence Hunt Date: Fri, 26 Apr 2019 17:00:51 +1000 Subject: [PATCH 3/8] chore: Update appveyor to use node 8 --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 29acaee..b46b6d9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ environment: - nodejs_version: "6" + nodejs_version: "8" # Install scripts. (runs after repo cloning) install: From 9c000abbd47f770607831b1760e9ac992c0f3d3d Mon Sep 17 00:00:00 2001 From: Lawrence Hunt Date: Sat, 27 Apr 2019 16:49:54 +1000 Subject: [PATCH 4/8] chore: Remove node 6 from build --- .circleci/config.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e278e7b..7b9ad64 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,10 +17,6 @@ jobs: path: "src/label/test-images" - store_artifacts: path: "src/resize/test-images" - test-node6: - <<: *test-common - docker: - - image: dwmkerr/node:6-imagemagick test-node8: <<: *test-common docker: @@ -38,11 +34,6 @@ workflows: version: 2 build: jobs: - - test-node6: - # All branches, all tags. - filters: - tags: - only: /.*/ - test-node8: # All branches, all tags. filters: From 4bc6b40afbe731f14aaef9d43c7756a363e1bd12 Mon Sep 17 00:00:00 2001 From: Lawrence Hunt Date: Sat, 27 Apr 2019 19:56:54 +1000 Subject: [PATCH 5/8] chore: Refactor app-icon.js to async/await --- bin/app-icon.js | 194 +++++++++++++++++++++++------------------------- 1 file changed, 92 insertions(+), 102 deletions(-) diff --git a/bin/app-icon.js b/bin/app-icon.js index 58f1802..a088a7f 100755 --- a/bin/app-icon.js +++ b/bin/app-icon.js @@ -1,8 +1,6 @@ #!/usr/bin/env node -// We use Node 6 to keep compatibility high, so need the 'use strict' statement. -// eslint-disable-next-line -'use strict'; +/* eslint-disable consistent-return */ const chalk = require('chalk'); const program = require('commander'); @@ -15,14 +13,13 @@ const labelImage = require('../src/label/label-image'); const fileExists = require('../src/utils/file-exists'); // Helper to throw an error if a file doesn't exist. -const errorIfMissing = (filePath, errorMessage) => { - return fileExists(filePath).then((exists) => { - if (!exists) { - console.error(`${chalk.red('error')}: ${errorMessage}`); - return process.exit(1); - } - return true; - }); +const errorIfMissing = async (filePath, errorMessage) => { + const exists = await fileExists(filePath); + if (!exists) { + console.error(`${chalk.red('error')}: ${errorMessage}`); + return process.exit(1); + } + return true; }; // Create the program. @@ -39,7 +36,7 @@ program .option('--background-icon [optional]', "The background icon path. Defaults to 'icon.background.png'") .option('--foreground-icon [optional]', "The foregroud icon path. Defaults to 'icon.foregroud.png'") .option('--adaptive-icons [optional]', "Additionally, generate Android Adaptive Icon templates. Defaults to 'false'") - .action((parameters) => { + .action(async (parameters) => { const { icon, backgroundIcon, @@ -48,43 +45,40 @@ program platforms, adaptiveIcons, } = parameters; - imagemagickCli.getVersion() - .then((version) => { - if (!version) { - console.error(' Error: ImageMagick must be installed. Try:'); - console.error(' brew install imagemagick'); - process.exit(1); - } - }) - .then(() => { - // Check we have the files we need. - const operations = []; - operations.push(errorIfMissing(icon, `Source file '${icon}' does not exist. Add the file or specify source icon with the '--icon' parameter.`)); - if (adaptiveIcons) { - const checkPath = backgroundIcon || 'icon.background.png'; - operations.push(errorIfMissing(checkPath, `Background icon file '${checkPath}' does not exist. Add the file or specify background icon with the '--background-icon' parameter.`)); - } - if (adaptiveIcons) { - const checkPath = foregroundIcon || 'icon.foreground.png'; - operations.push(errorIfMissing(checkPath, `Foreground icon file '${checkPath}' does not exist. Add the file or specify foreground icon with the '--foreground-icon' parameter.`)); - } - return Promise.all(operations); - }) - .then(() => { - // Generate some icons. - return generate({ - sourceIcon: icon, - backgroundIcon, - foregroundIcon, - searchRoot: search, - platforms, - adaptiveIcons, - }); - }) - .catch((generateErr) => { - console.error(chalk.red(`An error occurred generating the icons: ${generateErr.message}`)); - return process.exit(1); + + const version = await imagemagickCli.getVersion(); + + if (!version) { + console.error(' Error: ImageMagick must be installed. Try:'); + console.error(' brew install imagemagick'); + return process.exit(1); + } + + const operations = []; + operations.push(errorIfMissing(icon, `Source file '${icon}' does not exist. Add the file or specify source icon with the '--icon' parameter.`)); + if (adaptiveIcons) { + const checkPath = backgroundIcon || 'icon.background.png'; + operations.push(errorIfMissing(checkPath, `Background icon file '${checkPath}' does not exist. Add the file or specify background icon with the '--background-icon' parameter.`)); + } + if (adaptiveIcons) { + const checkPath = foregroundIcon || 'icon.foreground.png'; + operations.push(errorIfMissing(checkPath, `Foreground icon file '${checkPath}' does not exist. Add the file or specify foreground icon with the '--foreground-icon' parameter.`)); + } + try { + await Promise.all(operations); + + await generate({ + sourceIcon: icon, + backgroundIcon, + foregroundIcon, + searchRoot: search, + platforms, + adaptiveIcons, }); + } catch (err) { + console.error(chalk.red(`An error occurred generating the icons: ${err.message}`)); + process.exit(1); + } }); // Define the 'label' command. @@ -95,7 +89,7 @@ program .option('-o, --output ', "The output image, .e.g 'icon-out.png'.") .option('-t, --top [top]', "The label to put on the top of the image, .e.g 'qa'.") .option('-b, --bottom [bottom]', "The label to put on the bottom of the image, .e.g '1.2.5'.") - .action((parameters) => { + .action(async (parameters) => { const { input, output, @@ -103,30 +97,28 @@ program bottom, } = parameters; - imagemagickCli.getVersion() - .then((version) => { - if (!version) { - console.error(' Error: ImageMagick must be installed. Try:'); - console.error(' brew install imagemagick'); - return process.exit(1); - } - - // Check that we have a input file. - return fileExists(input); - }) - .then((exists) => { - if (!exists) { - console.error(`Input file '${input}' does not exist.`); - return process.exit(1); - } - // Generate some icons then innit. - return labelImage(input, output, top, bottom); - }) - .catch((labelErr) => { - console.error('An error occurred labelling the icon...'); - console.log(labelErr); - return process.exit(1); - }); + const version = await imagemagickCli.getVersion(); + + if (!version) { + console.error(' Error: ImageMagick must be installed. Try:'); + console.error(' brew install imagemagick'); + return process.exit(1); + } + + const exists = await fileExists(input); + + if (!exists) { + console.error(`Input file '${input}' does not exist.`); + return process.exit(1); + } + + try { + await labelImage(input, output, top, bottom); + } catch (err) { + console.error('An error occurred labelling the icon...'); + console.log(err); + process.exit(1); + } }); // Define the 'init' command. @@ -135,38 +127,36 @@ program .description('Initialises app icons by creating simple icon templates') .option('-c, --caption [caption]', "An optional caption for the icon, e.g 'App'.") .option('--adaptive-icons [optional]', "Additionally, generate Android Adaptive Icon templates. Defaults to 'false'") - .action((params) => { + .action(async (params) => { const { caption, adaptiveIcons } = params; - imagemagickCli.getVersion() - .then((version) => { - if (!version) { - console.error(' Error: ImageMagick must be installed. Try:'); - console.error(' brew install imagemagick'); - return process.exit(1); - } - - // Create the icon from the template, captioned if needed. - const input = path.resolve(__dirname, '../src/init/icon.template.png'); - return init(input, './icon.png', { caption }) - .then(() => console.log(`Created icon '${chalk.green('icon.png')}'`)); - }) - .then(() => { - // If we are going to use adaptive icons, create them. - if (!adaptiveIcons) return; + const version = await imagemagickCli.getVersion(); + + if (!version) { + console.error(' Error: ImageMagick must be installed. Try:'); + console.error(' brew install imagemagick'); + return process.exit(1); + } + + // Create the icon from the template, captioned if needed. + const input = path.resolve(__dirname, '../src/init/icon.template.png'); + + try { + await init(input, './icon.png', { caption }); + console.log(`Created icon '${chalk.green('icon.png')}'`); + + if (adaptiveIcons) { const inputBackground = path.resolve(__dirname, '../src/init/icon.background.template.png'); const inputForeground = path.resolve(__dirname, '../src/init/icon.foreground.template.png'); - init(inputBackground, './icon.background.png') - .then(() => init(inputForeground, './icon.foreground.png', { caption })) - .then(() => { - console.log(`Created icon '${chalk.green('icon.background.png')}'`); - console.log(`Created icon '${chalk.green('icon.foreground.png')}'`); - }); - }) - .catch((createError) => { - console.error('An error occurred creating the icon...'); - console.log(createError); - return process.exit(1); - }); + await init(inputBackground, './icon.background.png'); + await init(inputForeground, './icon.foreground.png', { caption }); + console.log(`Created icon '${chalk.green('icon.background.png')}'`); + console.log(`Created icon '${chalk.green('icon.foreground.png')}'`); + } + } catch (err) { + console.error('An error occurred creating the icon...'); + console.log(err); + return process.exit(1); + } }); // Extend the help with some examples. From b6b72ee031d56e7cd96e16c479b4e5eb7881166f Mon Sep 17 00:00:00 2001 From: Lawrence Hunt Date: Sat, 27 Apr 2019 20:49:53 +1000 Subject: [PATCH 6/8] chore: Update to async / await --- bin/app-icon.js | 13 ++--- .../generate-manifest-adaptive-icons.js | 51 +++++++++---------- src/init/init.js | 7 ++- src/label/label-image.js | 7 ++- src/utils/copy-file.js | 27 ---------- src/utils/delete-folder-if-exists.js | 12 ++--- src/utils/delete-folder-if-exists.specs.js | 23 ++++----- src/utils/delete-if-exists.js | 19 ++++--- src/utils/delete-if-exists.specs.js | 27 +++++----- src/utils/file-exists.js | 19 ++++--- src/utils/file-exists.specs.js | 27 +++++----- src/utils/find.specs.js | 37 +++++++------- 12 files changed, 118 insertions(+), 151 deletions(-) delete mode 100644 src/utils/copy-file.js diff --git a/bin/app-icon.js b/bin/app-icon.js index a088a7f..587dec4 100755 --- a/bin/app-icon.js +++ b/bin/app-icon.js @@ -14,12 +14,12 @@ const fileExists = require('../src/utils/file-exists'); // Helper to throw an error if a file doesn't exist. const errorIfMissing = async (filePath, errorMessage) => { - const exists = await fileExists(filePath); - if (!exists) { + try { + await fileExists(filePath); + } catch (err) { console.error(`${chalk.red('error')}: ${errorMessage}`); return process.exit(1); } - return true; }; // Create the program. @@ -105,12 +105,7 @@ program return process.exit(1); } - const exists = await fileExists(input); - - if (!exists) { - console.error(`Input file '${input}' does not exist.`); - return process.exit(1); - } + await errorIfMissing(input, `Input file '${input}' does not exist.`); try { await labelImage(input, output, top, bottom); diff --git a/src/android/generate-manifest-adaptive-icons.js b/src/android/generate-manifest-adaptive-icons.js index cd84252..c61a4af 100644 --- a/src/android/generate-manifest-adaptive-icons.js +++ b/src/android/generate-manifest-adaptive-icons.js @@ -2,10 +2,13 @@ const path = require('path'); const fs = require('fs'); const { EOL } = require('os'); const mkdirp = require('mkdirp'); +const { promisify } = require('util'); const androidManifestAdaptiveIcons = require('./AndroidManifest.adaptive-icons.json'); const resizeImage = require('../resize/resize-image'); +const mkdirpAsync = promisify(mkdirp); + // The XML for the ic launcher manifest. // eslint-disable-next-line const icLauncherManifestXml = @@ -29,32 +32,28 @@ module.exports = async function generateManifestIcons(backgroundIcon, foreground await Promise.all(androidManifestAdaptiveIcons.adaptiveIcons.map(async (icon) => { // Each icon lives in its own folder, so we'd better make sure that folder // exists. - return new Promise((resolve, reject) => { - const resourceFolder = path.join(manifestFolder, icon.folder); - mkdirp(resourceFolder, (err) => { - if (err) return reject(err); - - // Create the manifests, for the normal icons and round icons. - fs.writeFileSync(path.join(resourceFolder, 'ic_launcher.xml'), icLauncherManifestXml, 'utf8'); - fs.writeFileSync(path.join(resourceFolder, 'ic_launcher_round.xml'), icLauncherManifestXml, 'utf8'); - - const operations = []; - // If the manifest requires us to generate icons for the folder, do so. - // Not *every* folder has icons - for example the 'anydpi' folder will - // not contain icons. - if (icon.backgroundIcon) { - const backgroundOutput = path.join(resourceFolder, icon.backgroundIcon); - operations.push(resizeImage(backgroundIcon, backgroundOutput, icon.size)); - results.icons.push(backgroundOutput); - } - if (icon.foregroundIcon) { - const foregroundOutput = path.join(resourceFolder, icon.foregroundIcon); - operations.push(resizeImage(foregroundIcon, foregroundOutput, icon.size)); - results.icons.push(foregroundOutput); - } - return resolve(Promise.all(operations)); - }); - }); + const resourceFolder = path.join(manifestFolder, icon.folder); + await mkdirpAsync(resourceFolder); + + fs.writeFileSync(path.join(resourceFolder, 'ic_launcher.xml'), icLauncherManifestXml, 'utf8'); + fs.writeFileSync(path.join(resourceFolder, 'ic_launcher_round.xml'), icLauncherManifestXml, 'utf8'); + + const operations = []; + // If the manifest requires us to generate icons for the folder, do so. + // Not *every* folder has icons - for example the 'anydpi' folder will + // not contain icons. + if (icon.backgroundIcon) { + const backgroundOutput = path.join(resourceFolder, icon.backgroundIcon); + operations.push(resizeImage(backgroundIcon, backgroundOutput, icon.size)); + results.icons.push(backgroundOutput); + } + if (icon.foregroundIcon) { + const foregroundOutput = path.join(resourceFolder, icon.foregroundIcon); + operations.push(resizeImage(foregroundIcon, foregroundOutput, icon.size)); + results.icons.push(foregroundOutput); + } + + return Promise.all(operations); })); // Before writing the contents file, sort the contents (otherwise // they could be in a different order each time). diff --git a/src/init/init.js b/src/init/init.js index 75d031e..62ba543 100644 --- a/src/init/init.js +++ b/src/init/init.js @@ -1,6 +1,9 @@ +const fs = require('fs'); +const { promisify } = require('util'); const imagemagickCli = require('imagemagick-cli'); const getImageWidth = require('../imagemagick/get-image-width'); -const copyFile = require('../utils/copy-file'); + +const copyFileAsync = promisify(fs.copyFile); /** * init - creates an icon from a template. @@ -13,7 +16,7 @@ const copyFile = require('../utils/copy-file'); async function init(template, output, options) { // If there is no caption, then we can just copy the image. const caption = (options && options.caption) || ''; - if (!caption) return copyFile(template, output); + if (!caption) return copyFileAsync(template, output); // We have a caption, so we'll need to get the image width to work out how // to arrange it on the icon. diff --git a/src/label/label-image.js b/src/label/label-image.js index 11b54c5..b4c757a 100644 --- a/src/label/label-image.js +++ b/src/label/label-image.js @@ -1,6 +1,9 @@ +const fs = require('fs'); +const { promisify } = require('util'); const imagemagickCli = require('imagemagick-cli'); const getImageWidth = require('../imagemagick/get-image-width'); -const copyFile = require('../utils/copy-file'); + +const copyFileAsync = promisify(fs.copyFile); /** * caption - add a caption to an image. @@ -35,7 +38,7 @@ async function caption(input, output, label, gravity, proportionalSize) { // Single function to label an image (optionally top and bottom). module.exports = async function labelImage(input, output, top, bottom, middle) { // First, create the output image. This will overwrite any existing file. - await copyFile(input, output); + await copyFileAsync(input, output); // We'll have a set of promises which we will run, which will update the image. if (top) await caption(output, output, top, 'north', 0.2); diff --git a/src/utils/copy-file.js b/src/utils/copy-file.js deleted file mode 100644 index 37001bb..0000000 --- a/src/utils/copy-file.js +++ /dev/null @@ -1,27 +0,0 @@ -const fs = require('fs'); - -/** - * copyFile - copy 'source' to 'destination'. Needed as Node 6 doesn't have a - * native method. Once we move to Node 8 as the standard, we can retire this - * function. - * - * @param source - the path to the source file. - * @param target - the path to the target file. - * @returns - a promise which resolves when the file is copied, or rejects on an error. - */ -function copyFile(source, target) { - const rd = fs.createReadStream(source); - const wr = fs.createWriteStream(target); - return new Promise((resolve, reject) => { - rd.on('error', reject); - wr.on('error', reject); - wr.on('finish', resolve); - rd.pipe(wr); - }).catch((error) => { - rd.destroy(); - wr.end(); - throw error; - }); -} - -module.exports = copyFile; diff --git a/src/utils/delete-folder-if-exists.js b/src/utils/delete-folder-if-exists.js index ee50332..92e9fe5 100644 --- a/src/utils/delete-folder-if-exists.js +++ b/src/utils/delete-folder-if-exists.js @@ -1,11 +1,11 @@ const rimraf = require('rimraf'); +const { promisify } = require('util'); + +const rimrafAsync = promisify(rimraf); // Helper to delete a folder if it exists. -const deleteFolderIfExists = folder => new Promise((resolve, reject) => { - rimraf(folder, {}, (err) => { - if (err) return reject(err); - return resolve(); - }); -}); +async function deleteFolderIfExists(folder) { + return rimrafAsync(folder, {}); +} module.exports = deleteFolderIfExists; diff --git a/src/utils/delete-folder-if-exists.specs.js b/src/utils/delete-folder-if-exists.specs.js index abce664..ffd7333 100644 --- a/src/utils/delete-folder-if-exists.specs.js +++ b/src/utils/delete-folder-if-exists.specs.js @@ -4,22 +4,19 @@ const fs = require('fs'); const deleteFolderIfExists = require('./delete-folder-if-exists'); describe('delete-folder-if-exists', () => { - it('should be able to delete an existing folder', () => { + it('should be able to delete an existing folder', async () => { const testFolder = './temp-folder'; if (!fs.existsSync(testFolder)) fs.mkdirSync(testFolder); - return deleteFolderIfExists(testFolder) - .then(() => { - expect(fs.existsSync(testFolder)).to.equal(false); - }); + await deleteFolderIfExists(testFolder); + expect(fs.existsSync(testFolder)).to.equal(false); }); - it('should handle errors as rejections', () => { - return deleteFolderIfExists(undefined) - .then(() => { - assert.fail('deletion should fail - missing path'); - }) - .catch((err) => { - expect(err.message).to.match(/missing path/); - }); + it('should handle errors as rejections', async () => { + try { + await deleteFolderIfExists(undefined); + assert.fail('deletion should fail - missing path'); + } catch (err) { + expect(err.message).to.match(/missing path/); + } }); }); diff --git a/src/utils/delete-if-exists.js b/src/utils/delete-if-exists.js index 15164c1..a6fb9a0 100644 --- a/src/utils/delete-if-exists.js +++ b/src/utils/delete-if-exists.js @@ -1,13 +1,16 @@ const fs = require('fs'); +const { promisify } = require('util'); + +const unlinkAsync = promisify(fs.unlink); // Unlinks a file or folder. Resolves with true if a deletion occured, false // if no deletion occured, and rejets if there is an exception. -module.exports = function deleteIfExists(path) { - return new Promise((resolve, reject) => { - fs.unlink(path, (err) => { - if (err === null) return resolve(true); - if (err && err.code === 'ENOENT') return resolve(false); - return reject(err); - }); - }); +module.exports = async function deleteIfExists(path) { + try { + await unlinkAsync(path); + return true; + } catch (err) { + if (err && err.code === 'ENOENT') return false; + throw err; + } }; diff --git a/src/utils/delete-if-exists.specs.js b/src/utils/delete-if-exists.specs.js index a3af781..62caa8d 100644 --- a/src/utils/delete-if-exists.specs.js +++ b/src/utils/delete-if-exists.specs.js @@ -4,24 +4,21 @@ const deleteIfExists = require('./delete-if-exists'); describe('delete-if-exists', () => { fs.closeSync(fs.openSync('./temp.test', 'w')); - it('should be able to delete an existing file', () => { - return deleteIfExists('./temp.test') - .then((exists) => { - expect(exists).to.equal(true); - }); + it('should be able to delete an existing file', async () => { + const exists = await deleteIfExists('./temp.test'); + expect(exists).to.equal(true); }); - it('should be able to not delete a non-existant file', () => { - return deleteIfExists('./does-not-exist.test') - .then((exists) => { - expect(exists).to.equal(false); - }); + it('should be able to not delete a non-existant file', async () => { + const exists = await deleteIfExists('./does-not-exist.test'); + expect(exists).to.equal(false); }); - it('should be return a sensible error message with invalid input', () => { - return deleteIfExists(undefined) - .catch((err) => { - expect(err.message).to.match(/path.*string/); - }); + it('should be return a sensible error message with invalid input', async () => { + try { + await deleteIfExists(undefined); + } catch (err) { + expect(err.message).to.match(/path.*string/); + } }); }); diff --git a/src/utils/file-exists.js b/src/utils/file-exists.js index 591e5c0..da15cfe 100644 --- a/src/utils/file-exists.js +++ b/src/utils/file-exists.js @@ -1,11 +1,14 @@ const fs = require('fs'); +const { promisify } = require('util'); -module.exports = function fileExists(path) { - return new Promise((resolve, reject) => { - fs.stat(path, (err) => { - if (err === null) return resolve(true); - if (err && err.code === 'ENOENT') return resolve(false); - return reject(err); - }); - }); +const statAsync = promisify(fs.stat); + +module.exports = async function fileExists(path) { + try { + await statAsync(path); + return true; + } catch (err) { + if (err && err.code === 'ENOENT') return false; + throw err; + } }; diff --git a/src/utils/file-exists.specs.js b/src/utils/file-exists.specs.js index 1648dc4..4310aec 100644 --- a/src/utils/file-exists.specs.js +++ b/src/utils/file-exists.specs.js @@ -2,24 +2,21 @@ const { expect } = require('chai'); const fileExists = require('./file-exists'); describe('file-exists', () => { - it('should be able to check an existing file', () => { - return fileExists('./src/utils/file-exists.js') - .then((exists) => { - expect(exists).to.equal(true); - }); + it('should be able to check an existing file', async () => { + const exists = await fileExists('./src/utils/file-exists.js'); + expect(exists).to.equal(true); }); - it('should be able to check a non-existant file', () => { - return fileExists('./src/utils/file-does-not-exist.js') - .then((exists) => { - expect(exists).to.equal(false); - }); + it('should be able to check a non-existant file', async () => { + const exists = await fileExists('./src/utils/file-does-not-exist.js'); + expect(exists).to.equal(false); }); - it('should be return a sensible error message with invalid input', () => { - return fileExists(undefined) - .catch((err) => { - expect(err.message).to.match(/path.*string/); - }); + it('should be return a sensible error message with invalid input', async () => { + try { + await fileExists(undefined); + } catch (err) { + expect(err.message).to.match(/path.*string/); + } }); }); diff --git a/src/utils/find.specs.js b/src/utils/find.specs.js index 011afb0..5d95355 100644 --- a/src/utils/find.specs.js +++ b/src/utils/find.specs.js @@ -4,48 +4,45 @@ const path = require('path'); const find = require('./find'); describe('find', () => { - it('should be able to find a file', () => { - return find(path.normalize('./src'), (file, stat) => { + it('should be able to find a file', async () => { + const results = await find(path.normalize('./src'), (file, stat) => { return file.match(/find.js$/) && !stat.isDirectory(); - }).then((results) => { - expect(results.length).to.equal(1); - expect(results[0]).to.match(/find.js$/); }); + expect(results.length).to.equal(1); + expect(results[0]).to.match(/find.js$/); }); - it('should be able to find a folder', () => { + it('should be able to find a folder', async () => { // Build the regexp for src/utils, cross platform. const folder = path.normalize('src/utils').replace(/\\/g, '\\\\'); const rex = new RegExp(`${folder}$`); - return find(path.normalize('./src'), (file, stat) => { + const results = await find(path.normalize('./src'), (file, stat) => { return file.match(rex) && stat.isDirectory(); - }).then((results) => { - expect(results.length).to.equal(1); - expect(results[0]).to.match(rex); }); + expect(results.length).to.equal(1); + expect(results[0]).to.match(rex); }); - it('should reject if an invalid search root is passed', () => { + it('should reject if an invalid search root is passed', async () => { // TODO: A potential improvement here might be to use chai-as-promised // and explicitly expect a rejection. Otherwise, if the function // succeeds (by mistake), we won't see it as an explicit failure. - return find(path.normalize('./invalid')) - .catch((err) => { - expect(err.message).to.match(/no such file or directory/); - }); + try { + await find(path.normalize('./invalid')); + } catch (err) { + expect(err.message).to.match(/no such file or directory/); + } }); - it('should return an empty resultset for empty directories', () => { + it('should return an empty resultset for empty directories', async () => { // TODO: The find function has a different codepath for empty folders. // Better would be a more functional pattern, then iterating through a // fileset works with same whether or not it is empty. // Ensure we have an empty directory first! mkdirp.sync(path.normalize('./src/utils/empty')); - return find(path.normalize('./src/utils/empty')) - .then((results) => { - expect(results.length).to.equal(0); - }); + const results = await find(path.normalize('./src/utils/empty')); + expect(results.length).to.equal(0); }); }); From 2f0b77979f5951d5a132f8d69cd8d57c7edf4a47 Mon Sep 17 00:00:00 2001 From: Lawrence Hunt Date: Mon, 29 Apr 2019 08:56:06 +1000 Subject: [PATCH 7/8] chore: Add in node eslint plugin and fix issues --- .eslintrc.yml | 5 ++++- package-lock.json | 32 ++++++++++++++++++++++++++++++++ package.json | 3 ++- src/validate-parameters.specs.js | 12 ++++++------ 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 5650ec8..de322d5 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,4 +1,6 @@ -extends: airbnb-base +extends: + - airbnb-base + - plugin:node/recommended env: node: true mocha: true @@ -10,3 +12,4 @@ rules: # tests, so relax the requirement to have single return statements arrow-body-style: 0 import/no-extraneous-dependencies: [error, { devDependencies: ['**/*.specs.js'] }] + no-process-exit: 0 diff --git a/package-lock.json b/package-lock.json index 11b7f49..224312b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1039,6 +1039,16 @@ } } }, + "eslint-plugin-es": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-1.4.0.tgz", + "integrity": "sha512-XfFmgFdIUDgvaRAlaXUkxrRg5JSADoRC8IkKLc/cISeR3yHVMefFHQZpcyXXEUUPHfy5DwviBcrfqlyqEwlQVw==", + "dev": true, + "requires": { + "eslint-utils": "^1.3.0", + "regexpp": "^2.0.1" + } + }, "eslint-plugin-import": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz", @@ -1084,6 +1094,28 @@ } } }, + "eslint-plugin-node": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-8.0.1.tgz", + "integrity": "sha512-ZjOjbjEi6jd82rIpFSgagv4CHWzG9xsQAVp1ZPlhRnnYxcTgENUVBvhYmkQ7GvT1QFijUSo69RaiOJKhMu6i8w==", + "dev": true, + "requires": { + "eslint-plugin-es": "^1.3.1", + "eslint-utils": "^1.3.1", + "ignore": "^5.0.2", + "minimatch": "^3.0.4", + "resolve": "^1.8.1", + "semver": "^5.5.0" + }, + "dependencies": { + "ignore": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.1.tgz", + "integrity": "sha512-DWjnQIFLenVrwyRCKZT+7a7/U4Cqgar4WG8V++K3hw+lrW1hc/SIwdiGmtxKCVACmHULTuGeBbHJmbwW7/sAvA==", + "dev": true + } + } + }, "eslint-restricted-globals": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz", diff --git a/package.json b/package.json index 6f71e54..38c3b5f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Automatic icon resizing for Mobile Apps. Supports Native, Cordova and React Native. Inspired by cordova-icon.", "main": "./src/index.js", "engines": { - "node": ">=8" + "node": ">=8.5" }, "bin": { "app-icon": "./bin/app-icon.js" @@ -39,6 +39,7 @@ "eslint": "^5.9.0", "eslint-config-airbnb-base": "^13.1.0", "eslint-plugin-import": "^2.14.0", + "eslint-plugin-node": "^8.0.1", "mocha": "^6.1.4", "nyc": "^14.0.0", "standard-version": "^5.0.0" diff --git a/src/validate-parameters.specs.js b/src/validate-parameters.specs.js index f500bc0..5ee2c26 100644 --- a/src/validate-parameters.specs.js +++ b/src/validate-parameters.specs.js @@ -17,42 +17,42 @@ describe('validateParameters', () => { const params = validParameters(); delete params.sourceIcon; const parameters = validateParameters(params); - assert.equal(parameters.sourceIcon, 'icon.png'); + assert.strictEqual(parameters.sourceIcon, 'icon.png'); }); it('should provide a default background icon', () => { const params = validParameters(); delete params.backgroundIcon; const parameters = validateParameters(params); - assert.equal(parameters.backgroundIcon, 'icon.background.png'); + assert.strictEqual(parameters.backgroundIcon, 'icon.background.png'); }); it('should provide a default foreground icon', () => { const params = validParameters(); delete params.foregroundIcon; const parameters = validateParameters(params); - assert.equal(parameters.foregroundIcon, 'icon.foreground.png'); + assert.strictEqual(parameters.foregroundIcon, 'icon.foreground.png'); }); it('should provide a default search root', () => { const params = validParameters(); delete params.searchRoot; const parameters = validateParameters(params); - assert.equal(parameters.searchRoot, './'); + assert.strictEqual(parameters.searchRoot, './'); }); it('should provide a default adaptive icons option', () => { const params = validParameters(); delete params.adaptiveIcons; const parameters = validateParameters(params); - assert.equal(parameters.adaptiveIcons, false); + assert.strictEqual(parameters.adaptiveIcons, false); }); it('should provide a default set of platforms', () => { const params = validParameters(); delete params.platforms; const parameters = validateParameters(params); - assert.deepEqual(parameters.platforms, ['android', 'ios']); + assert.deepStrictEqual(parameters.platforms, ['android', 'ios']); }); it('should reject invalid platforms', () => { From be1c9f52a8ecdbc42ed466741cae5d2cf3067801 Mon Sep 17 00:00:00 2001 From: Lawrence Hunt Date: Mon, 29 Apr 2019 09:22:44 +1000 Subject: [PATCH 8/8] chore: Refactor code to take advantage of async/await --- bin/app-icon.js | 42 +++++++------------ .../generate-manifest-adaptive-icons.js | 7 ++-- src/generate.js | 30 +++++++------ 3 files changed, 33 insertions(+), 46 deletions(-) diff --git a/bin/app-icon.js b/bin/app-icon.js index 587dec4..8e65574 100755 --- a/bin/app-icon.js +++ b/bin/app-icon.js @@ -22,6 +22,16 @@ const errorIfMissing = async (filePath, errorMessage) => { } }; +const imageMagickCheck = async () => { + const version = await imagemagickCli.getVersion(); + + if (!version) { + console.error(' Error: ImageMagick must be installed. Try:'); + console.error(' brew install imagemagick'); + return process.exit(1); + } +}; + // Create the program. program .version(pack.version); @@ -46,27 +56,18 @@ program adaptiveIcons, } = parameters; - const version = await imagemagickCli.getVersion(); - - if (!version) { - console.error(' Error: ImageMagick must be installed. Try:'); - console.error(' brew install imagemagick'); - return process.exit(1); - } + await imageMagickCheck(); - const operations = []; - operations.push(errorIfMissing(icon, `Source file '${icon}' does not exist. Add the file or specify source icon with the '--icon' parameter.`)); + await errorIfMissing(icon, `Source file '${icon}' does not exist. Add the file or specify source icon with the '--icon' parameter.`); if (adaptiveIcons) { const checkPath = backgroundIcon || 'icon.background.png'; - operations.push(errorIfMissing(checkPath, `Background icon file '${checkPath}' does not exist. Add the file or specify background icon with the '--background-icon' parameter.`)); + await errorIfMissing(checkPath, `Background icon file '${checkPath}' does not exist. Add the file or specify background icon with the '--background-icon' parameter.`); } if (adaptiveIcons) { const checkPath = foregroundIcon || 'icon.foreground.png'; - operations.push(errorIfMissing(checkPath, `Foreground icon file '${checkPath}' does not exist. Add the file or specify foreground icon with the '--foreground-icon' parameter.`)); + await errorIfMissing(checkPath, `Foreground icon file '${checkPath}' does not exist. Add the file or specify foreground icon with the '--foreground-icon' parameter.`); } try { - await Promise.all(operations); - await generate({ sourceIcon: icon, backgroundIcon, @@ -97,13 +98,7 @@ program bottom, } = parameters; - const version = await imagemagickCli.getVersion(); - - if (!version) { - console.error(' Error: ImageMagick must be installed. Try:'); - console.error(' brew install imagemagick'); - return process.exit(1); - } + await imageMagickCheck(); await errorIfMissing(input, `Input file '${input}' does not exist.`); @@ -124,13 +119,8 @@ program .option('--adaptive-icons [optional]', "Additionally, generate Android Adaptive Icon templates. Defaults to 'false'") .action(async (params) => { const { caption, adaptiveIcons } = params; - const version = await imagemagickCli.getVersion(); - if (!version) { - console.error(' Error: ImageMagick must be installed. Try:'); - console.error(' brew install imagemagick'); - return process.exit(1); - } + await imageMagickCheck(); // Create the icon from the template, captioned if needed. const input = path.resolve(__dirname, '../src/init/icon.template.png'); diff --git a/src/android/generate-manifest-adaptive-icons.js b/src/android/generate-manifest-adaptive-icons.js index c61a4af..1c98472 100644 --- a/src/android/generate-manifest-adaptive-icons.js +++ b/src/android/generate-manifest-adaptive-icons.js @@ -38,22 +38,21 @@ module.exports = async function generateManifestIcons(backgroundIcon, foreground fs.writeFileSync(path.join(resourceFolder, 'ic_launcher.xml'), icLauncherManifestXml, 'utf8'); fs.writeFileSync(path.join(resourceFolder, 'ic_launcher_round.xml'), icLauncherManifestXml, 'utf8'); - const operations = []; // If the manifest requires us to generate icons for the folder, do so. // Not *every* folder has icons - for example the 'anydpi' folder will // not contain icons. if (icon.backgroundIcon) { const backgroundOutput = path.join(resourceFolder, icon.backgroundIcon); - operations.push(resizeImage(backgroundIcon, backgroundOutput, icon.size)); + await resizeImage(backgroundIcon, backgroundOutput, icon.size); results.icons.push(backgroundOutput); } if (icon.foregroundIcon) { const foregroundOutput = path.join(resourceFolder, icon.foregroundIcon); - operations.push(resizeImage(foregroundIcon, foregroundOutput, icon.size)); + await resizeImage(foregroundIcon, foregroundOutput, icon.size); results.icons.push(foregroundOutput); } - return Promise.all(operations); + return null; })); // Before writing the contents file, sort the contents (otherwise // they could be in a different order each time). diff --git a/src/generate.js b/src/generate.js index 33d3cec..49c81ce 100644 --- a/src/generate.js +++ b/src/generate.js @@ -42,24 +42,22 @@ module.exports = async function generate(parameters) { await Promise.all(manifests.map(async (manifest) => { if (!platforms.includes('android')) return null; console.log(`Found Android Manifest: ${manifest}...`); - const operations = [ - generateManifestIcons(sourceIcon, manifest).then(({ icons }) => { - results.manifests.push({ manifest, icons }); - icons.forEach((icon) => { - console.log(` ${chalk.green('✓')} Generated icon ${icon}`); - }); - }), - ]; + + const manResult = await generateManifestIcons(sourceIcon, manifest); + results.manifests.push({ manifest, icons: manResult.icons }); + manResult.icons.forEach((icon) => { + console.log(` ${chalk.green('✓')} Generated icon ${icon}`); + }); + if (adaptiveIcons) { - operations.push(generateManifestAdaptiveIcons(backgroundIcon, foregroundIcon, manifest) - .then(({ icons }) => { - results.adaptiveIconManifests.push({ manifest, icons }); - icons.forEach((icon) => { - console.log(` ${chalk.green('✓')} Generated adaptive icon ${icon}`); - }); - })); + const atvRes = await generateManifestAdaptiveIcons(backgroundIcon, foregroundIcon, manifest); + results.adaptiveIconManifests.push({ manifest, icons: atvRes.icons }); + atvRes.icons.forEach((icon) => { + console.log(` ${chalk.green('✓')} Generated adaptive icon ${icon}`); + }); } - return Promise.all(operations); + + return null; })); return results; };