├── dist └── .gitkeep ├── .npmignore ├── .gitignore ├── node-tests ├── helpers │ └── expect.js ├── fixtures │ ├── splash.svg │ ├── icon.svg │ └── config.xml │ │ ├── no-platform-nodes.xml │ │ ├── ios-platform-node.xml │ │ ├── no-and-ios-platform-node-expected.xml │ │ ├── android-platform-node.xml │ │ └── android-platform-node-expected.xml └── unit │ ├── utils │ ├── validate-platforms-test.js │ ├── make-dir-test.js │ ├── serialize-splash-test.js │ ├── write-images-test.js │ ├── serialize-icon-test.js │ └── update-config-test.js │ ├── icon-task-test.js │ └── splash-task-test.js ├── src ├── utils │ ├── abort-task.js │ ├── get-platform-sizes.js │ ├── validate-platforms.js │ ├── make-dir.js │ ├── serialize-splash.js │ ├── serialize-icon.js │ ├── write-images.js │ ├── svg2png.js │ ├── svg2png-converter.js │ └── update-config.js ├── platform-splash-sizes.js ├── splash-task.js ├── icon-task.js └── platform-icon-sizes.js ├── .travis.yml ├── bin ├── splicon-icons.js └── splicon-splashes.js ├── LICENSE.md ├── package.json └── README.md /dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | dist/* 3 | !dist/.gitkeep 4 | -------------------------------------------------------------------------------- /node-tests/helpers/expect.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const chaiAsPromised = require('chai-as-promised'); 3 | 4 | chai.use(chaiAsPromised); 5 | 6 | module.exports = chai.expect; -------------------------------------------------------------------------------- /src/utils/abort-task.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, esversion: 6 */ 2 | 'use strict'; 3 | 4 | const chalk = require('chalk'); 5 | 6 | module.exports = function(msg) { 7 | console.log(chalk.red(`${msg}. Aborting`)); 8 | process.exit(); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/get-platform-sizes.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, esversion: 6 */ 2 | 'use strict'; 3 | 4 | module.exports = function(platformSizes, platforms) { 5 | let _platformSizes = []; 6 | 7 | platforms.forEach((platform) => { 8 | _platformSizes[platform] = platformSizes[platform]; 9 | }); 10 | 11 | return _platformSizes; 12 | }; 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | - "6" 5 | 6 | sudo: false 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | matrix: 13 | fast_finish: true 14 | 15 | before_install: 16 | - npm config set spin false 17 | - npm install -g bower phantomjs-prebuilt 18 | 19 | install: 20 | - npm install 21 | 22 | script: 23 | - npm test 24 | -------------------------------------------------------------------------------- /src/utils/validate-platforms.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, esversion: 6 */ 2 | const _filter = require('lodash').filter; 3 | const ALLOWED_PLATFORMS = [ 4 | 'all', 5 | 'ios', 6 | 'android', 7 | 'blackberry', 8 | 'windows' 9 | ]; 10 | 11 | module.exports = function(platforms) { 12 | const invalidPlatforms = _filter(platforms, (item) => { 13 | return ALLOWED_PLATFORMS.indexOf(item) === -1; 14 | }); 15 | 16 | return invalidPlatforms.length === 0; 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/make-dir.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, esversion: 6 */ 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | 5 | module.exports = function (base, destPath) { 6 | destPath = destPath.split("/"); 7 | destPath.forEach((segment) => { 8 | if (segment) { 9 | base = path.join(base, segment); 10 | try { 11 | fs.mkdirSync(base); 12 | } catch (e) { 13 | if (e.code !== "EEXIST") { 14 | throw e; 15 | } 16 | } 17 | } 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/serialize-splash.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, esversion: 6 */ 2 | 'use strict'; 3 | 4 | const _get = require('lodash').get; 5 | 6 | module.exports = function (platform, projectPath, iconData) { 7 | let props = { 8 | id: iconData.id, 9 | src: iconData.path 10 | }; 11 | 12 | if (platform === 'ios') { 13 | props.width = iconData.width.toString(); 14 | props.height = iconData.height.toString(); 15 | } else if (platform === 'android') { 16 | props.density = iconData.id; 17 | } 18 | 19 | return props; 20 | }; 21 | -------------------------------------------------------------------------------- /bin/splicon-icons.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | /* jshint node:true, esversion: 6 */ 4 | 'use strict'; 5 | 6 | //TODO - better opts parsing, allow source/dest 7 | 8 | const IconTask = require('../src/icon-task'); 9 | const chalk = require('chalk'); 10 | 11 | (function() { 12 | const args = process.argv.slice(2); 13 | console.log(chalk.green( 14 | `Generating cordova icons for ${args}` 15 | )); 16 | 17 | IconTask({platforms: args}).then(() => { 18 | console.log(chalk.green( 19 | "Done!" 20 | )); 21 | }); 22 | })(); 23 | -------------------------------------------------------------------------------- /bin/splicon-splashes.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | /* jshint node:true, esversion: 6 */ 4 | 'use strict'; 5 | 6 | //TODO - better opts parsing, allow source/dest 7 | 8 | const SplashTask = require('../src/splash-task'); 9 | const chalk = require('chalk'); 10 | 11 | (function() { 12 | const args = process.argv.slice(2); 13 | console.log(chalk.green( 14 | `Generating cordova splashes for ${args}` 15 | )); 16 | 17 | SplashTask({platforms: args}).then(() => { 18 | console.log(chalk.green( 19 | "Done!" 20 | )); 21 | }); 22 | })(); 23 | -------------------------------------------------------------------------------- /node-tests/fixtures/splash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | splicon 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/utils/serialize-icon.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, esversion: 6 */ 2 | 'use strict'; 3 | 4 | const _get = require('lodash').get; 5 | 6 | module.exports = function (platform, projectPath, iconData) { 7 | let props = { 8 | id: iconData.id, 9 | src: iconData.path 10 | }; 11 | 12 | if (platform === 'ios') { 13 | props.height = iconData.size.toString(); 14 | props.width = iconData.size.toString(); 15 | } else if (platform === 'android') { 16 | props.density = iconData.id; 17 | } else if (platform === 'windows') { 18 | const target = _get(iconData, 'attrs.target', iconData.id); 19 | props.target = target; 20 | } 21 | 22 | return props; 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /node-tests/fixtures/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /node-tests/unit/utils/validate-platforms-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('../../helpers/expect'); 4 | 5 | const ValidatePlatforms = require('../../../src/utils/validate-platforms'); 6 | 7 | describe('ValidatePlatforms', () => { 8 | context('when platforms contain valid platforms', () => { 9 | const platforms = ['all', 'ios', 'android', 'blackberry', 'windows']; 10 | 11 | it('returns true', () => { 12 | expect(ValidatePlatforms(platforms)).to.equal(true); 13 | }); 14 | }); 15 | 16 | context('when platforms contain invalid platforms', () => { 17 | const platforms = ['symbian']; 18 | 19 | it('returns false', () => { 20 | expect(ValidatePlatforms(platforms)).to.equal(false); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /node-tests/unit/utils/make-dir-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const td = require('testdouble'); 4 | 5 | const MakeDir = require('../../../src/utils/make-dir'); 6 | 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | 10 | describe('MakeDir', () => { 11 | context('when base and destPath', () => { 12 | let mkdirSync; 13 | 14 | beforeEach(() => { 15 | mkdirSync = td.replace(fs, 'mkdirSync'); 16 | }); 17 | 18 | afterEach(() => { 19 | td.reset(); 20 | }); 21 | 22 | it('makes the directory', () => { 23 | let base = './'; 24 | const destPath = 'foo/bar'; 25 | 26 | MakeDir(base, destPath); 27 | 28 | // Verify replaced property was invoked. 29 | destPath.split('/').forEach((segment) => { 30 | base = path.join(base, segment); 31 | 32 | td.verify(mkdirSync(base)); 33 | }); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /node-tests/fixtures/config.xml/no-platform-nodes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | emberCordovaExample 4 | 5 | A sample Apache Cordova application that responds to the deviceready event. 6 | 7 | 8 | Apache Cordova Team 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /node-tests/fixtures/config.xml/ios-platform-node.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | emberCordovaExample 4 | 5 | A sample Apache Cordova application that responds to the deviceready event. 6 | 7 | 8 | Apache Cordova Team 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Isle of Code Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /node-tests/fixtures/config.xml/no-and-ios-platform-node-expected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | emberCordovaExample 4 | 5 | A sample Apache Cordova application that responds to the deviceready event. 6 | 7 | 8 | Apache Cordova Team 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /node-tests/fixtures/config.xml/android-platform-node.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | emberCordovaExample 4 | 5 | A sample Apache Cordova application that responds to the deviceready event. 6 | 7 | 8 | Apache Cordova Team 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /node-tests/fixtures/config.xml/android-platform-node-expected.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | emberCordovaExample 4 | 5 | A sample Apache Cordova application that responds to the deviceready event. 6 | 7 | 8 | Apache Cordova Team 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /node-tests/unit/utils/serialize-splash-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('../../helpers/expect'); 4 | 5 | const SerializeSplash = require('../../../src/utils/serialize-splash'); 6 | 7 | describe('SerializeSplash', () => { 8 | context('when platform is iOS', () => { 9 | it('returns an object with id, path, width, and height', () => { 10 | const platform = 'ios'; 11 | const projectPath = ''; 12 | const iconData = { 13 | path: 'foo', 14 | width: 1536, 15 | height: 2048, 16 | id: '1536-2048' 17 | }; 18 | 19 | const props = SerializeSplash(platform, projectPath, iconData); 20 | 21 | expect(props.id).to.equal(iconData.id); 22 | expect(props.src).to.equal(iconData.path); 23 | expect(props.width).to.equal(iconData.width.toString()); 24 | expect(props.height).to.equal(iconData.height.toString()); 25 | }); 26 | }); 27 | 28 | context('when platform is Android', () => { 29 | it('returns an object with id, path, and density', () => { 30 | const platform = 'android'; 31 | const projectPath = ''; 32 | const iconData = { 33 | path: 'foo', 34 | width: 1920, 35 | height: 1280, 36 | id: 'land-xxxhdpi' 37 | }; 38 | 39 | const props = SerializeSplash(platform, projectPath, iconData); 40 | 41 | expect(props.id).to.equal(iconData.id); 42 | expect(props.src).to.equal(iconData.path); 43 | expect(props.density).to.equal(iconData.id); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/utils/write-images.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, esversion: 6 */ 2 | 'use strict'; 3 | 4 | const MakeDir = require('./make-dir'); 5 | const svg2png = require('./svg2png'); 6 | const fs = require('fs'); 7 | const RSVP = require('rsvp'); 8 | const normalizePath = require('path').normalize; 9 | const _forOwn = require('lodash').forOwn; 10 | const _union = require('lodash').union; 11 | 12 | module.exports = function (opts) { 13 | return new RSVP.Promise((resolve) => { 14 | let buffer = fs.readFileSync(opts.source); 15 | let rasterizeTasks = []; 16 | let rasterizeQueue = []; 17 | 18 | _forOwn(opts.platformSizes, (icons, platform) => { 19 | MakeDir('./', `${opts.projectPath}/${opts.dest}/${platform}`); 20 | 21 | icons.sizes.map((size) => { 22 | size.path = `${opts.dest}/${platform}/${size.id}.png`; 23 | }); 24 | 25 | rasterizeQueue = _union(rasterizeQueue, icons.sizes); 26 | }); 27 | 28 | rasterizeQueue.forEach((rasterize) => { 29 | let width, height; 30 | 31 | if (rasterize.size) { 32 | width = rasterize.size; 33 | height = rasterize.size; 34 | } else { 35 | width = rasterize.width; 36 | height = rasterize.height; 37 | } 38 | 39 | let rasterizeTask = svg2png(buffer, { width: width, height: height }) 40 | .then((pngBuffer) => { 41 | const writePath = `${opts.projectPath}/${rasterize.path}`; 42 | fs.writeFileSync(normalizePath(writePath), pngBuffer); 43 | }) 44 | 45 | rasterizeTasks.push(rasterizeTask); 46 | }); 47 | 48 | RSVP.all(rasterizeTasks).then(() => resolve(opts.platformSizes)); 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /src/utils/svg2png.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, esversion: 6 */ 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | const childProcess = require('pn/child_process'); 6 | 7 | module.exports = (sourceBuffer, resize) => { 8 | const phantomjsCmd = require('phantomjs-prebuilt').path; 9 | const converterFileName = path.resolve(__dirname, './svg2png-converter.js'); 10 | 11 | const cp = childProcess.execFile( 12 | phantomjsCmd, 13 | [converterFileName, JSON.stringify(resize)], 14 | { maxBuffer: Infinity } 15 | ); 16 | 17 | writeBufferInChunks(cp.stdin, sourceBuffer); 18 | 19 | return cp.promise.then(processResult); 20 | }; 21 | 22 | function writeBufferInChunks(writableStream, buffer) { 23 | const asString = buffer.toString('base64'); 24 | const chunkSize = 1024; 25 | 26 | writableStream.cork(); 27 | 28 | for (let offset = 0; offset < asString.length; offset += chunkSize) { 29 | writableStream.write(asString.substring(offset, offset + chunkSize)); 30 | } 31 | 32 | writableStream.end("\n"); // so that the PhantomJS side can use readLine() 33 | } 34 | 35 | function processResult(result) { 36 | const prefix = 'data:image/png;base64,'; 37 | const stdout = result.stdout.toString(); 38 | 39 | if (stdout.startsWith(prefix)) { 40 | return new Buffer(stdout.substring(prefix.length), 'base64'); 41 | } 42 | 43 | if (stdout.length > 0) { 44 | // PhantomJS always outputs to stdout. 45 | throw new Error(stdout.replace(/\r/g, '').trim()); 46 | } 47 | 48 | const stderr = result.stderr.toString(); 49 | 50 | if (stderr.length > 0) { 51 | // But hey something else might get to stderr. 52 | throw new Error(stderr.replace(/\r/g, '').trim()); 53 | } 54 | 55 | throw new Error('No data received from the PhantomJS child process'); 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "splicon", 3 | "version": "0.0.15", 4 | "description": "Generate cordova/splash files from a single svg, and update config.xml", 5 | "homepage": "https://github.com/isleofcode/splicon", 6 | "main": "src/icon-task.js", 7 | "scripts": { 8 | "test": "./node_modules/.bin/mocha 'node-tests/**/*-test.js'", 9 | "compile": "./node_modules/babel-cli/bin/babel.js --presets babel-preset-es2015 --browserify -d dist/ src/", 10 | "prepublish": "npm run compile" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/isleofcode/splicon.git" 15 | }, 16 | "bin": { 17 | "splicon-icons": "bin/splicon-icons.js", 18 | "splicon-splashes": "bin/splicon-splashes.js" 19 | }, 20 | "keywords": [ 21 | "cordova", 22 | "phonegap", 23 | "splash", 24 | "icon" 25 | ], 26 | "author": { 27 | "name": "Alex Blom", 28 | "email": "alex@isleofcode.com", 29 | "url": "https://isleofcode.com" 30 | }, 31 | "contributors": [ 32 | { 33 | "name": "Chris Thoburn", 34 | "email": "chris@isleofcode.com", 35 | "url": "https://isleofcode.com" 36 | }, 37 | { 38 | "name": "Jordan Yee", 39 | "email": "jordan@isleofcode.com", 40 | "url": "https://isleofcode.com" 41 | } 42 | ], 43 | "license": "MIT", 44 | "dependencies": { 45 | "chalk": "^0.4.0", 46 | "lodash": "^4.13.1", 47 | "phantomjs-prebuilt": "^2.1.12", 48 | "pn": "^1.0.0", 49 | "rsvp": "^3.2.1", 50 | "xml2js": "^0.4.16" 51 | }, 52 | "devDependencies": { 53 | "babel-cli": "^6.14.0", 54 | "babel-preset-es2015": "^6.14.0", 55 | "babelify": "^7.3.0", 56 | "chai": "^3.5.0", 57 | "chai-as-promised": "^5.3.0", 58 | "image-size": "0.3.5", 59 | "mocha": "^2.5.3", 60 | "testdouble": "^1.4.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/platform-splash-sizes.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, esversion: 6 */ 2 | 3 | module.exports = { 4 | ios: { 5 | sizes: [ 6 | { width: 640, height: 960, id: '640-960' }, 7 | { width: 960, height: 640, id: '960-640' }, 8 | 9 | { width: 640, height: 1136, id: '640-1136' }, 10 | { width: 1136, height: 640, id: '1136-640' }, 11 | 12 | { width: 750, height: 1334, id: '750-1334' }, 13 | { width: 1334, height: 750, id: '1334-750' }, 14 | 15 | { width: 1242, height: 2208, id: '1242-2208' }, 16 | { width: 2208, height: 1242, id: '2208-1242' }, 17 | 18 | { width: 768, height: 1024, id: '768-1024' }, 19 | { width: 1024, height: 768, id: '1024-768' }, 20 | 21 | { width: 1536, height: 2048, id: '1536-2048' }, 22 | { width: 2048, height: 1536, id: '2048-1536' }, 23 | 24 | { width: 2048, height: 2732, id: '2048-2732' }, 25 | { width: 2732, height: 2048, id: '2732-2048' }, 26 | 27 | { width: 1125, height: 2436, id: '1125-2436' }, 28 | { width: 2436, height: 1125, id: '2436-1125' } 29 | ] 30 | }, 31 | 32 | android: { 33 | sizes: [ 34 | { width: 200, height: 320, id: 'port-ldpi' }, 35 | { width: 320, height: 200, id: 'land-ldpi' }, 36 | 37 | { width: 320, height: 480, id: 'port-mdpi' }, 38 | { width: 480, height: 320, id: 'land-mdpi' }, 39 | 40 | { width: 480, height: 800, id: 'port-hdpi' }, 41 | { width: 800, height: 480, id: 'land-hdpi' }, 42 | 43 | { width: 720, height: 1280, id: 'port-xhdpi' }, 44 | { width: 1280, height: 720, id: 'land-xhdpi' }, 45 | 46 | { width: 960, height: 1600, id: 'port-xxhdpi' }, 47 | { width: 1600, height: 960, id: 'land-xxhdpi' }, 48 | 49 | { width: 1280, height: 1920, id: 'port-xxxhdpi' }, 50 | { width: 1920, height: 1280, id: 'land-xxxhdpi' } 51 | ] 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/splash-task.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, esversion: 6 */ 2 | 'use strict'; 3 | 4 | const AbortTask = require('./utils/abort-task'); 5 | const GetPlatSizes = require('./utils/get-platform-sizes'); 6 | const PlatformSizes = require('./platform-splash-sizes'); 7 | const SerialSplash = require('./utils/serialize-splash'); 8 | const UpdateConfig = require('./utils/update-config'); 9 | const ValidPlatform = require('./utils/validate-platforms'); 10 | const WriteImages = require('./utils/write-images'); 11 | 12 | const RSVP = require('rsvp'); 13 | const existsSync = require('fs').existsSync; 14 | const _defaults = require('lodash').defaults; 15 | 16 | module.exports = function(opts) { 17 | return new RSVP.Promise((resolve) => { 18 | if (opts === undefined) opts = {}; 19 | 20 | _defaults(opts, { 21 | source: 'splash.svg', 22 | dest: 'res/screen', 23 | projectPath: './cordova', 24 | platforms: ['all'] 25 | }); 26 | 27 | if (!existsSync(opts.source)) { 28 | AbortTask(`Source splash ${opts.source} does not exist`); 29 | } 30 | 31 | if (!ValidPlatform(opts.platforms)) { 32 | AbortTask(`Platforms ${opts.platforms} are not all valid`); 33 | } 34 | 35 | if (opts.platforms.length === 0 || opts.platforms.indexOf('all') > -1) { 36 | opts.platforms = ['ios', 'android']; 37 | } 38 | 39 | const platformSizes = GetPlatSizes(PlatformSizes, opts.platforms); 40 | 41 | WriteImages({ 42 | source: opts.source, 43 | projectPath: opts.projectPath, 44 | dest: opts.dest, 45 | platformSizes: platformSizes 46 | }) 47 | .then((updatedPlatformSizes) => { 48 | UpdateConfig({ 49 | projectPath: opts.projectPath, 50 | desiredNodes: updatedPlatformSizes, 51 | keyName: 'splash', 52 | serializeFn: SerialSplash 53 | }) 54 | }) 55 | .then(resolve); 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /src/icon-task.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, esversion: 6 */ 2 | 'use strict'; 3 | 4 | const AbortTask = require('./utils/abort-task'); 5 | const GetPlatSizes = require('./utils/get-platform-sizes'); 6 | const PlatformSizes = require('./platform-icon-sizes'); 7 | const SerializeIcon = require('./utils/serialize-icon'); 8 | const UpdateConfig = require('./utils/update-config'); 9 | const ValidPlatform = require('./utils/validate-platforms'); 10 | const WriteImages = require('./utils/write-images'); 11 | 12 | const RSVP = require('rsvp'); 13 | const existsSync = require('fs').existsSync; 14 | const _defaults = require('lodash').defaults; 15 | 16 | module.exports = function(opts) { 17 | return new RSVP.Promise((resolve) => { 18 | if (opts === undefined) opts = {}; 19 | 20 | _defaults(opts, { 21 | source: 'icon.svg', 22 | dest: 'res/icon', 23 | projectPath: './cordova', 24 | platforms: ['all'] 25 | }); 26 | 27 | if (!existsSync(opts.source)) { 28 | AbortTask(`Source icon ${opts.source} does not exist`); 29 | } 30 | 31 | if (!ValidPlatform(opts.platforms)) { 32 | AbortTask(`Platforms ${opts.platforms} are not all valid`); 33 | } 34 | 35 | if (opts.platforms.length === 0 || opts.platforms.indexOf('all') > -1) { 36 | opts.platforms = ['ios', 'android', 'windows', 'blackberry']; 37 | } 38 | 39 | const platformSizes = GetPlatSizes(PlatformSizes, opts.platforms); 40 | 41 | WriteImages({ 42 | source: opts.source, 43 | projectPath: opts.projectPath, 44 | dest: opts.dest, 45 | platformSizes: platformSizes 46 | }) 47 | .then((updatedPlatformSizes) => { 48 | UpdateConfig({ 49 | projectPath: opts.projectPath, 50 | desiredNodes: updatedPlatformSizes, 51 | keyName: 'icon', 52 | serializeFn: SerializeIcon 53 | }) 54 | }) 55 | .then(resolve); 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /src/platform-icon-sizes.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true, esversion: 6 */ 2 | 3 | module.exports = { 4 | ios: { 5 | sizes: [ 6 | { size: 40, id: 'icon-40' }, 7 | { size: 57, id: 'icon' }, 8 | { size: 80, id: 'icon-40@2x' }, 9 | { size: 120, id: 'icon-40@3x' }, 10 | { size: 60, id: 'icon-60' }, 11 | { size: 120, id: 'icon-60@2x' }, 12 | { size: 180, id: 'icon-60@3x' }, 13 | { size: 114, id: 'icon@2x' }, 14 | { size: 29, id: 'icon-small' }, 15 | { size: 58, id: 'icon-small@2x' }, 16 | { size: 87, id: 'icon-small@3x' }, 17 | { size: 152, id: 'icon-76@2x' }, 18 | { size: 76, id: 'icon-76' }, 19 | { size: 72, id: 'icon-72' }, 20 | { size: 144, id: 'icon-72@2x' }, 21 | { size: 50, id: 'icon-50' }, 22 | { size: 100, id: 'icon-50@2x' }, 23 | { size: 167, id: 'icon-83.5@2x' }, 24 | { size: 120, id: 'icon-120' }, 25 | { size: 240, id: 'icon-120@2x' }, 26 | { size: 152, id: 'icon-152' }, 27 | { size: 304, id: 'icon-152@2x' }, 28 | { size: 20, id: 'icon-20' }, 29 | { size: 48, id: 'icon-24@2x' }, 30 | { size: 55, id: 'icon-27.5@2x' }, 31 | { size: 88, id: 'icon-44@2x' }, 32 | { size: 172, id: 'icon-86@2x' }, 33 | { size: 196, id: 'icon-98@2x' }, 34 | { size: 1024, id: 'icon-1024' } 35 | ] 36 | }, 37 | 38 | android: { 39 | sizes: [ 40 | { size: 36, id: 'ldpi' }, 41 | { size: 48, id: 'mdpi' }, 42 | { size: 72, id: 'hdpi' }, 43 | { size: 96, id: 'xhdpi' }, 44 | { size: 144, id: 'xxhdpi' }, 45 | { size: 192, id: 'xxxhdpi' } 46 | ] 47 | }, 48 | 49 | blackberry: { 50 | sizes: [ 51 | { size: 86, id: 'icon-86' }, 52 | { size: 150, id: 'icon-150' } 53 | ] 54 | }, 55 | 56 | windows: { 57 | sizes: [ 58 | { size: 50, id: 'StoreLogo' }, 59 | { size: 30, id: 'smalllogo', attrs: { target: 'Square30x30Logo' }}, 60 | { size: 44, id: 'Square44x44Logo' }, 61 | { size: 70, id: 'Square70x70Logo' }, 62 | { size: 71, id: 'Square71x71Logo' }, 63 | { size: 150, id: 'Square150x150Logo' }, 64 | { size: 310, id: 'Square310x310Logo' } 65 | ] 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/utils/svg2png-converter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global phantom: true */ 4 | 5 | var webpage = require('webpage'); 6 | var system = require('system'); 7 | 8 | if (system.args.length !== 2) { 9 | console.error('Usage: converter.js resize'); 10 | phantom.exit(); 11 | } else { 12 | convert(system.args[1]); 13 | } 14 | 15 | function convert(resize) { 16 | var page = webpage.create(); 17 | var sourceBase64 = system.stdin.readLine(); 18 | 19 | page.open('data:image/svg+xml;base64,' + sourceBase64, function (status) { 20 | if (status !== 'success') { 21 | console.error('Unable to load the source file.'); 22 | phantom.exit(); 23 | return; 24 | } 25 | 26 | try { 27 | resize = JSON.parse(resize); 28 | 29 | var cropRequired = resize.width !== resize.height; 30 | 31 | if (cropRequired) { 32 | var crop = { width: resize.width, height: resize.height }; 33 | var maxDimension = crop.width > crop.height ? 'width' : 'height'; 34 | var maxDimensionSize = crop[maxDimension]; 35 | resize.width = maxDimensionSize; 36 | resize.height = maxDimensionSize; 37 | } 38 | 39 | setSVGDimensions(page, resize.width, resize.height); 40 | 41 | page.viewportSize = { 42 | width: resize.width, 43 | height: resize.height 44 | }; 45 | 46 | if (cropRequired) { 47 | var top, left; 48 | 49 | if (maxDimension == 'width') { 50 | top = (resize.height - crop.height) / 2; 51 | left = 0; 52 | } else { 53 | top = 0; 54 | left = (resize.width - crop.width) / 2; 55 | } 56 | 57 | page.clipRect = { 58 | top: top, 59 | left: left, 60 | width: crop.width, 61 | height: crop.height 62 | }; 63 | } 64 | } catch (e) { 65 | console.error('Unable to set dimensions.'); 66 | console.error(e); 67 | phantom.exit(); 68 | return; 69 | } 70 | 71 | var result = 'data:image/png;base64,' + page.renderBase64('PNG'); 72 | system.stdout.write(result); 73 | phantom.exit(); 74 | }); 75 | } 76 | 77 | function setSVGDimensions(page, width, height) { 78 | return page.evaluate(function (width, height) { 79 | /* global document: true */ 80 | var el = document.documentElement; 81 | el.setAttribute('width', width + 'px'); 82 | el.setAttribute('height', height + 'px'); 83 | }, width, height); 84 | } 85 | -------------------------------------------------------------------------------- /node-tests/unit/utils/write-images-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('../../helpers/expect'); 4 | 5 | const WriteImages = require('../../../src/utils/write-images'); 6 | 7 | const fs = require('fs'); 8 | const sizeOf = require('image-size'); 9 | const _forOwn = require('lodash').forOwn; 10 | 11 | describe('WriteImages', function() { 12 | // Hitting the file system is slow 13 | this.timeout(0); 14 | 15 | before(() => { 16 | if (!fs.existsSync('tmp')) fs.mkdirSync('tmp'); 17 | }); 18 | 19 | context('when source, projectPath, dest, and platformSizes', () => { 20 | const source = 'node-tests/fixtures/icon.svg'; 21 | const projectPath = 'tmp'; 22 | const dest = 'icons'; 23 | const platformSizes = { 24 | ios: { 25 | sizeKey: 'width', 26 | sizes: [ 27 | { 28 | size: 57, 29 | id: 'icon' 30 | } 31 | ] 32 | } 33 | }; 34 | let subject; 35 | 36 | before(() => { 37 | subject = WriteImages({ 38 | source: source, 39 | projectPath: projectPath, 40 | dest: dest, 41 | platformSizes: platformSizes 42 | }); 43 | }); 44 | 45 | after(() => { 46 | platformSizes['ios'].sizes.forEach((rasterize) => { 47 | fs.unlinkSync(`${projectPath}/${rasterize.path}`); 48 | }); 49 | }); 50 | 51 | it('resolves to platform sizes updated with paths', (done) => { 52 | subject.then((updatedPlatformSizes) => { 53 | try { 54 | _forOwn(updatedPlatformSizes, (icons, platform) => { 55 | icons.sizes.map((size) => { 56 | const path = `${dest}/${platform}/${size.id}.png`; 57 | expect(size.path).to.equal(path); 58 | }); 59 | }); 60 | done(); 61 | } catch(e) { 62 | done(e); 63 | } 64 | }); 65 | }); 66 | 67 | it('writes the files to rasterize at the right size', (done) => { 68 | subject.then((updatedPlatformSizes) => { 69 | try { 70 | updatedPlatformSizes['ios'].sizes.forEach((rasterize) => { 71 | const writePath = `${projectPath}/${rasterize.path}`; 72 | expect(fs.existsSync(writePath)).to.equal(true); 73 | expect(sizeOf(writePath).width).to.equal(rasterize.size); 74 | expect(sizeOf(writePath).height).to.equal(rasterize.size); 75 | }); 76 | done(); 77 | } catch(e) { 78 | done(e); 79 | } 80 | }); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Splicon (beta) 2 | -------------- 3 | [![Build 4 | Status](https://travis-ci.org/isleofcode/splicon.svg?branch=master)](https://travis-ci.org/isleofcode/splicon) 5 | 6 | Splicon is a command-line tool and library for generating icons and splash 7 | screens for Cordova projects. It generates the images for each platform's 8 | required sizes using a single source SVG and updates the project's config.xml. 9 | 10 | It was originally built for use in [ember-cordova](https://github.com/isleofcode/ember-cordova). 11 | 12 | It is built for Node 0.12+ but may work on earlier versions. 13 | 14 | ## Icons 15 | 16 | For an integration example, see the [ember-cordova `make-icon` command](https://github.com/isleofcode/ember-cordova/tree/master/lib/commands/make-icons.js). 17 | 18 | Using the CLI, from your Cordova project, run: 19 | 20 | ``` 21 | splicon-icons 22 | ``` 23 | 24 | This command will: 25 | 26 | 1. Look for a file called 'icon.svg'; 27 | 2. Resize the SVG for each required platform/icon combination; 28 | 3. Move the icons to res/icons/platformName (and create the dir if it does not 29 | exist); 30 | 4. Update your config.xml to represent the new icons & paths; 31 | 5. Ensure there are no duplicate icon nodes in config.xml; 32 | 33 | By default, images for all platforms will be generated. To generate images for 34 | specific platforms you can pass the platforms as arguments: 35 | 36 | ``` 37 | splicon-icons ios android windows 38 | ``` 39 | 40 | For more granular control (such as setting the destination path), you 41 | will need to require src/icon-task and run the function yourself. 42 | 43 | There is a TODO to enhance CLI flag, but in most cases this is handled in 44 | [ember-cordova](https://github.com/isleofcode/ember-cordova). 45 | 46 | ## Splash Screens 47 | 48 | ``` 49 | splicon-splashes 50 | ``` 51 | 52 | Like `splicon-icons`, by default images for all platforms will be generated. To 53 | generate images for specific platforms you can pass the platforms as arguments: 54 | 55 | ``` 56 | splicon-splashes ios 57 | ``` 58 | 59 | ## Testing 60 | 61 | ``` 62 | npm test 63 | ``` 64 | 65 | ## Contributing 66 | 67 | PRs are very welcome. You can read our style guides 68 | [here](https://github.com/isleofcode/style-guide). 69 | 70 | If you are unsure about your contribution idea, please feel free to open an 71 | issue for feedback. 72 | 73 | ## Credits 74 | 75 | [ember-cordova](https://github.com/isleofcode/ember-cordova) is maintained by 76 | [Isle of Code](https://isleofcode.com) and was written by Alex Blom and Jordan 77 | Yee based on work by Chris Thoburn and Alex Blom. 78 | -------------------------------------------------------------------------------- /node-tests/unit/utils/serialize-icon-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('../../helpers/expect'); 4 | 5 | const SerializeIcon = require('../../../src/utils/serialize-icon'); 6 | 7 | describe('SerializeIcon', () => { 8 | context('when platform is iOS', () => { 9 | it('returns an object with id, path, width, and height', () => { 10 | const platform = 'ios'; 11 | const projectPath = ''; 12 | const iconData = { path: 'foo', size: 180, id: 'foo' }; 13 | 14 | const props = SerializeIcon(platform, projectPath, iconData); 15 | 16 | expect(props.id).to.equal(iconData.id); 17 | expect(props.src).to.equal(iconData.path); 18 | expect(props.width).to.equal(iconData.size.toString()); 19 | expect(props.height).to.equal(iconData.size.toString()); 20 | }); 21 | }); 22 | 23 | context('when platform is Android', () => { 24 | it('returns an object with id, path, and density', () => { 25 | const platform = 'android'; 26 | const projectPath = ''; 27 | const iconData = { path: 'foo', id: 'xxxhdpi' }; 28 | 29 | const props = SerializeIcon(platform, projectPath, iconData); 30 | 31 | expect(props.id).to.equal(iconData.id); 32 | expect(props.src).to.equal(iconData.path); 33 | expect(props.density).to.equal(iconData.id); 34 | }); 35 | }); 36 | 37 | context('when platform is Windows', () => { 38 | const platform = 'windows'; 39 | const projectPath = ''; 40 | 41 | context('when icon data includes id and target', () => { 42 | it('returns an object with id, path, and target as target', () => { 43 | const iconData = { 44 | path: 'foo', 45 | id: 'smalllogo', 46 | attrs: { 47 | target: 'Square30x30Logo' 48 | } 49 | }; 50 | 51 | const props = SerializeIcon(platform, projectPath, iconData); 52 | 53 | expect(props.id).to.equal(iconData.id); 54 | expect(props.src).to.equal(iconData.path); 55 | expect(props.target).to.equal(iconData.attrs.target); 56 | }); 57 | }) 58 | 59 | context('when icon data includes id and not target', () => { 60 | it('returns an object with id, path, and id as target', () => { 61 | const iconData = { path: 'foo', id: 'smalllogo' }; 62 | 63 | const props = SerializeIcon(platform, projectPath, iconData); 64 | 65 | expect(props.id).to.equal(iconData.id); 66 | expect(props.src).to.equal(iconData.path); 67 | expect(props.target).to.equal(iconData.id); 68 | }); 69 | }) 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/utils/update-config.js: -------------------------------------------------------------------------------- 1 | /* globals String */ 2 | 3 | /* jshint node:true, esversion: 6 */ 4 | 'use strict'; 5 | 6 | const xml2js = require('xml2js'); 7 | const fs = require('fs'); 8 | const RSVP = require('rsvp'); 9 | const chalk = require('chalk'); 10 | const _findIndex = require('lodash').findIndex; 11 | const _forOwn = require('lodash').forOwn; 12 | const _filter = require('lodash').filter; 13 | const _remove = require('lodash').remove; 14 | const _pick = require('lodash').pick; 15 | const _pullAt = require('lodash').pullAt; 16 | 17 | const parseXML = function(xmlPath) { 18 | return new RSVP.Promise((resolve, reject) => { 19 | const contents = fs.readFileSync(xmlPath, 'utf8'); 20 | const parser = new xml2js.Parser(); 21 | 22 | if (contents === '') reject('File is empty'); 23 | 24 | parser.parseString(contents, function (err, result) { 25 | if (err) reject(err); 26 | if (result) resolve(result); 27 | }); 28 | }); 29 | }; 30 | 31 | const saveXML = function(json, xmlPath) { 32 | const builder = new xml2js.Builder({ 33 | renderOpts: { 34 | 'pretty': true, 35 | 'indent': ' ', 36 | 'newline': '\n' 37 | } 38 | }); 39 | const xml = builder.buildObject(json); 40 | 41 | // Add missing trailing newline 42 | fs.writeFileSync(xmlPath, xml + '\n'); 43 | }; 44 | 45 | const addNodes = function(json, opts) { 46 | _forOwn(opts.desiredNodes, (nodeData, platformName) => { 47 | 48 | //Cordova wont always have a platforms: [] 49 | if (!json.widget.platform) json.widget.platform = []; 50 | 51 | //See if platform already exists 52 | let platformNode; 53 | let platformNodePos = _findIndex(json.widget.platform, {$: { name: platformName } }); 54 | 55 | if (platformNodePos > -1) { 56 | //the platform existed, assign to platform Node & temp rm from JSOn 57 | platformNode = json.widget.platform[platformNodePos]; 58 | _pullAt(json.widget.platform, platformNodePos); 59 | } else { 60 | platformNode = {$: { name: platformName } }; 61 | } 62 | 63 | let targetNodes = platformNode[opts.keyName]; 64 | if (targetNodes === undefined) { 65 | targetNodes = []; 66 | } 67 | 68 | // Replace existing nodes. 69 | nodeData.sizes.forEach((node) => { 70 | let newAttrs = opts.serializeFn(platformName, opts.projectPath, node); 71 | 72 | _filter(targetNodes, (item) => { 73 | if (!item) return; 74 | 75 | let existingAttrs = item.$; 76 | 77 | if (newAttrs.id === existingAttrs.id) { 78 | _remove(targetNodes, item); 79 | } 80 | }); 81 | 82 | targetNodes.push( {$: newAttrs} ); 83 | }); 84 | 85 | platformNode[opts.keyName] = targetNodes; 86 | json.widget.platform.push(platformNode); 87 | }); 88 | 89 | return json; 90 | }; 91 | 92 | /* 93 | Required opts: 94 | 95 | desiredNodes Array: 96 | {ios: [], android: [], blackberry: []} 97 | See src/platform-icon-sizes for an example 98 | 99 | keyName: String 100 | `icon` or `splash` 101 | 102 | serializeFn: Function 103 | Given a single node from desiredNodes, serialize to config.xml format 104 | */ 105 | module.exports = function(opts) { 106 | const configPath = `${opts.projectPath}/config.xml`; 107 | 108 | return new RSVP.Promise((resolve, reject) => { 109 | if (!opts.projectPath || 110 | !opts.desiredNodes || 111 | !opts.keyName || 112 | !opts.serializeFn) { 113 | 114 | reject( 115 | 'Missing required opts: projectPath, desiredNodes, keyName, serializeFn' 116 | ); 117 | } 118 | 119 | parseXML(configPath).then((json) => { 120 | json = addNodes(json, opts); 121 | saveXML(json, configPath); 122 | resolve(); 123 | }).catch((err) => { 124 | console.log(chalk.red( 125 | `Error reading XML: ${err}` 126 | )); 127 | 128 | reject(new Error(err)); 129 | }); 130 | }); 131 | }; 132 | -------------------------------------------------------------------------------- /node-tests/unit/icon-task-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('../helpers/expect'); 4 | 5 | const IconTask = require('../../src/icon-task'); 6 | 7 | const fs = require('fs'); 8 | const sizeOf = require('image-size'); 9 | const parseString = require('xml2js').parseString; 10 | const _find = require('lodash').find; 11 | const PlatformSizes = require('../../src/platform-icon-sizes'); 12 | 13 | describe('IconTask', function() { 14 | // Hitting the file system is slow 15 | this.timeout(0); 16 | 17 | const configFixtureDir = 'node-tests/fixtures/config.xml'; 18 | const tmpConfigPath = 'tmp/config.xml'; 19 | 20 | const svgPath = 'node-tests/fixtures/icon.svg'; 21 | const pngPath = 'icons'; 22 | const projectPath = 'tmp'; 23 | 24 | before((done) => { 25 | if (!fs.existsSync('tmp')) fs.mkdirSync('tmp'); 26 | 27 | const fixturePath = `${configFixtureDir}/no-platform-nodes.xml`; 28 | const fixtureStream = fs.createReadStream(fixturePath); 29 | const tmpConfigStream = fs.createWriteStream(tmpConfigPath); 30 | fixtureStream.pipe(tmpConfigStream); 31 | tmpConfigStream.on('finish', () => done()); 32 | }); 33 | 34 | after(() => { 35 | fs.unlinkSync(tmpConfigPath); 36 | }); 37 | 38 | context('when platforms', () => { 39 | context('when ios', () => { 40 | const platform = 'ios'; 41 | const platformSizes = PlatformSizes[platform]; 42 | let task; 43 | 44 | before(() => { 45 | task = IconTask({ 46 | source: svgPath, 47 | dest: pngPath, 48 | projectPath: projectPath, 49 | platforms: [platform] 50 | }) 51 | }); 52 | 53 | after(() => { 54 | platformSizes.sizes.forEach((size) => { 55 | const path = 56 | `${projectPath}/${pngPath}/${platform}/${size.id}.png`; 57 | fs.unlinkSync(path); 58 | }); 59 | fs.rmdirSync(`${projectPath}/${pngPath}/${platform}`); 60 | fs.rmdirSync(`${projectPath}/${pngPath}`); 61 | }); 62 | 63 | it('writes the icons', (done) => { 64 | task.then(() => { 65 | try { 66 | platformSizes.sizes.forEach((size) => { 67 | const path = 68 | `${projectPath}/${pngPath}/${platform}/${size.id}.png`; 69 | 70 | expect(fs.existsSync(path)).to.equal(true); 71 | expect(sizeOf(path).width).to.equal(size.size); 72 | expect(sizeOf(path).height).to.equal(size.size); 73 | }); 74 | done(); 75 | } catch(e) { 76 | done(e); 77 | } 78 | }); 79 | }); 80 | 81 | it('updates config.xml', (done) => { 82 | task.then(() => { 83 | const configFile = fs.readFileSync(tmpConfigPath, 'utf8'); 84 | 85 | try { 86 | parseString(configFile, (err, config) => { 87 | if (err) done(err); 88 | 89 | const platformNode = _find(config.widget.platform, (node) => { 90 | return node.$.name == platform; 91 | }); 92 | 93 | expect(platformNode).to.exist; 94 | 95 | const iconsAttrs = platformNode.icon.map((iconNode) => { 96 | return iconNode.$; 97 | }); 98 | 99 | platformSizes.sizes.forEach((size) => { 100 | const attrs = { 101 | id: size.id, 102 | src: `${pngPath}/${platform}/${size.id}.png`, 103 | height: size.size.toString(), 104 | width: size.size.toString() 105 | } 106 | 107 | expect(iconsAttrs).to.include(attrs); 108 | }); 109 | }); 110 | done(); 111 | } catch(e) { 112 | done(e); 113 | } 114 | }); 115 | }); 116 | 117 | it('returns a promise', (done) => { 118 | expect(task).to.be.fulfilled.notify(done); 119 | }); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /node-tests/unit/splash-task-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('../helpers/expect'); 4 | 5 | const SplashTask = require('../../src/splash-task'); 6 | 7 | const fs = require('fs'); 8 | const sizeOf = require('image-size'); 9 | const parseString = require('xml2js').parseString; 10 | const _find = require('lodash').find; 11 | const PlatformSizes = require('../../src/platform-splash-sizes'); 12 | 13 | describe('SplashTask', function() { 14 | // Hitting the file system is slow 15 | this.timeout(0); 16 | 17 | const configFixtureDir = 'node-tests/fixtures/config.xml'; 18 | const tmpConfigPath = 'tmp/config.xml'; 19 | 20 | const svgPath = 'node-tests/fixtures/splash.svg'; 21 | const pngPath = 'splashes'; 22 | const projectPath = 'tmp'; 23 | 24 | before((done) => { 25 | if (!fs.existsSync('tmp')) fs.mkdirSync('tmp'); 26 | 27 | const fixturePath = `${configFixtureDir}/no-platform-nodes.xml`; 28 | const fixtureStream = fs.createReadStream(fixturePath); 29 | const tmpConfigStream = fs.createWriteStream(tmpConfigPath); 30 | fixtureStream.pipe(tmpConfigStream); 31 | tmpConfigStream.on('finish', () => done()); 32 | }); 33 | 34 | after(() => { 35 | fs.unlinkSync(tmpConfigPath); 36 | }); 37 | 38 | context('when platforms', () => { 39 | context('when ios', () => { 40 | const platform = 'ios'; 41 | const platformSizes = PlatformSizes[platform]; 42 | let task; 43 | 44 | before(() => { 45 | task = SplashTask({ 46 | source: svgPath, 47 | dest: pngPath, 48 | projectPath: projectPath, 49 | platforms: [platform] 50 | }) 51 | }); 52 | 53 | after(() => { 54 | platformSizes.sizes.forEach((size) => { 55 | const path = 56 | `${projectPath}/${pngPath}/${platform}/${size.id}.png`; 57 | fs.unlinkSync(path); 58 | }); 59 | fs.rmdirSync(`${projectPath}/${pngPath}/${platform}`); 60 | fs.rmdirSync(`${projectPath}/${pngPath}`); 61 | }); 62 | 63 | it('writes the splashes', (done) => { 64 | task.then(() => { 65 | try { 66 | platformSizes.sizes.forEach((size) => { 67 | const path = 68 | `${projectPath}/${pngPath}/${platform}/${size.id}.png`; 69 | 70 | expect(fs.existsSync(path)).to.equal(true); 71 | expect(sizeOf(path).width).to.equal(size.width); 72 | expect(sizeOf(path).height).to.equal(size.height); 73 | }); 74 | done(); 75 | } catch(e) { 76 | done(e); 77 | } 78 | }); 79 | }); 80 | 81 | it('updates config.xml', (done) => { 82 | task.then(() => { 83 | const configFile = fs.readFileSync(tmpConfigPath, 'utf8'); 84 | 85 | try { 86 | parseString(configFile, (err, config) => { 87 | if (err) done(err); 88 | 89 | const platformNode = _find(config.widget.platform, (node) => { 90 | return node.$.name == platform; 91 | }); 92 | 93 | expect(platformNode).to.exist; 94 | 95 | const splashesAttrs = platformNode.splash.map((splashNode) => { 96 | return splashNode.$; 97 | }); 98 | 99 | platformSizes.sizes.forEach((size) => { 100 | const attrs = { 101 | id: size.id, 102 | src: `${pngPath}/${platform}/${size.id}.png`, 103 | height: size.height.toString(), 104 | width: size.width.toString() 105 | } 106 | 107 | expect(splashesAttrs).to.include(attrs); 108 | }); 109 | }); 110 | done(); 111 | } catch(e) { 112 | done(e); 113 | } 114 | }); 115 | }); 116 | 117 | it('returns a promise', (done) => { 118 | expect(task).to.be.fulfilled.notify(done); 119 | }); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /node-tests/unit/utils/update-config-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('../../helpers/expect'); 4 | 5 | const UpdateConfig = require('../../../src/utils/update-config'); 6 | 7 | const fs = require('fs'); 8 | const SerializeIcon = require('../../../src/utils/serialize-icon'); 9 | 10 | describe('UpdateConfig', function() { 11 | const configFixtureDir = 'node-tests/fixtures/config.xml'; 12 | const tmpConfigPath = 'tmp/config.xml'; 13 | 14 | before(() => { 15 | if (!fs.existsSync('tmp')) fs.mkdirSync('tmp'); 16 | }); 17 | 18 | afterEach(() => { 19 | fs.unlinkSync(tmpConfigPath); 20 | }); 21 | 22 | context('when projectPath, desiredNodes, keyName, and serializeFn', () => { 23 | const projectPath = 'tmp'; 24 | const desiredNodes = { 25 | ios: { 26 | sizes: [ 27 | { size: 57, id: 'icon', path: 'res/icon/ios/icon.png' } 28 | ] 29 | } 30 | }; 31 | const keyName = 'icon'; 32 | const serializeFn = SerializeIcon; 33 | const args = { 34 | projectPath: projectPath, 35 | desiredNodes: desiredNodes, 36 | keyName: keyName, 37 | serializeFn: serializeFn 38 | }; 39 | 40 | context('when config.xml has no platform nodes', () => { 41 | before((done) => { 42 | const fixturePath = `${configFixtureDir}/no-platform-nodes.xml`; 43 | const fixtureStream = fs.createReadStream(fixturePath); 44 | const tmpConfigStream = fs.createWriteStream(tmpConfigPath); 45 | fixtureStream.pipe(tmpConfigStream); 46 | tmpConfigStream.on('finish', () => { done(); }); 47 | }); 48 | 49 | it('it adds the platform node with icon nodes', (done) => { 50 | UpdateConfig(args).then(() => { 51 | const tmpConfig = fs.readFileSync(tmpConfigPath, 'utf8'); 52 | const expectedConfigPath = 53 | `${configFixtureDir}/no-and-ios-platform-node-expected.xml`; 54 | const expectedConfig = fs.readFileSync(expectedConfigPath, 'utf8'); 55 | 56 | try { 57 | expect(tmpConfig).to.equal(expectedConfig); 58 | done(); 59 | } catch(e) { 60 | done(e); 61 | } 62 | }).catch((e) => { 63 | done(e); 64 | }); 65 | }); 66 | }); 67 | 68 | context('when config.xml has desiredNodes platform with no icons', () => { 69 | before((done) => { 70 | const fixturePath = `${configFixtureDir}/ios-platform-node.xml`; 71 | const fixtureStream = fs.createReadStream(fixturePath); 72 | const tmpConfigStream = fs.createWriteStream(tmpConfigPath); 73 | fixtureStream.pipe(tmpConfigStream); 74 | tmpConfigStream.on('finish', () => { done(); }); 75 | }); 76 | 77 | it('it adds the icon nodes', (done) => { 78 | UpdateConfig(args).then(() => { 79 | const tmpConfig = fs.readFileSync(tmpConfigPath, 'utf8'); 80 | const expectedConfigPath = 81 | `${configFixtureDir}/no-and-ios-platform-node-expected.xml`; 82 | const expectedConfig = fs.readFileSync(expectedConfigPath, 'utf8'); 83 | 84 | try { 85 | expect(tmpConfig).to.equal(expectedConfig); 86 | done(); 87 | } catch(e) { 88 | done(e); 89 | } 90 | }).catch((e) => { 91 | done(e); 92 | }); 93 | }); 94 | }); 95 | 96 | context('when config.xml has desiredNodes platform with icons', () => { 97 | before((done) => { 98 | const fixturePath = 99 | `${configFixtureDir}/no-and-ios-platform-node-expected.xml`; 100 | const fixtureStream = fs.createReadStream(fixturePath); 101 | const tmpConfigStream = fs.createWriteStream(tmpConfigPath); 102 | fixtureStream.pipe(tmpConfigStream); 103 | tmpConfigStream.on('finish', () => { done(); }); 104 | }); 105 | 106 | it('it replaces the icon nodes', (done) => { 107 | UpdateConfig(args).then(() => { 108 | const tmpConfig = fs.readFileSync(tmpConfigPath, 'utf8'); 109 | const expectedConfigPath = 110 | `${configFixtureDir}/no-and-ios-platform-node-expected.xml`; 111 | const expectedConfig = fs.readFileSync(expectedConfigPath, 'utf8'); 112 | 113 | try { 114 | expect(tmpConfig).to.equal(expectedConfig); 115 | done(); 116 | } catch(e) { 117 | done(e); 118 | } 119 | }).catch((e) => { 120 | done(e); 121 | }); 122 | }); 123 | }); 124 | 125 | context('when config.xml does not have desiredNodes platform', () => { 126 | before((done) => { 127 | const fixturePath = `${configFixtureDir}/android-platform-node.xml`; 128 | const fixtureStream = fs.createReadStream(fixturePath); 129 | const tmpConfigStream = fs.createWriteStream(tmpConfigPath); 130 | fixtureStream.pipe(tmpConfigStream); 131 | tmpConfigStream.on('finish', () => { done(); }); 132 | }); 133 | 134 | it('it adds the platform node with icon nodes', (done) => { 135 | UpdateConfig(args).then(() => { 136 | const tmpConfig = fs.readFileSync(tmpConfigPath, 'utf8'); 137 | const expectedConfigPath = 138 | `${configFixtureDir}/android-platform-node-expected.xml`; 139 | const expectedConfig = fs.readFileSync(expectedConfigPath, 'utf8'); 140 | 141 | try { 142 | expect(tmpConfig).to.equal(expectedConfig); 143 | done(); 144 | } catch(e) { 145 | done(e); 146 | } 147 | }).catch((e) => { 148 | done(e); 149 | }); 150 | }); 151 | }); 152 | }); 153 | }); 154 | --------------------------------------------------------------------------------