ionic
Version:
A tool for creating and developing Ionic Framework mobile apps.
901 lines (733 loc) • 26.8 kB
JavaScript
var fs = require('fs'),
os = require('os'),
path = require('path'),
Q = require('q'),
argv = require('optimist').argv,
request = require('request'),
xml2js = require('xml2js'),
_ = require('underscore'),
crc = require('crc'),
resSettings = require('./settings.js'),
Task = require('../task').Task,
shelljs = require('shelljs'),
moduleSettings = require('../../../package.json'),
IonicAppLib = require('ionic-app-lib'),
Utils = IonicAppLib.utils,
IonicStats = require('../stats').IonicStats;
var IonicResources = function() {};
IonicResources.prototype = new Task();
var settings = resSettings.ResSettings;
var platformConfigs = resSettings.ResPlatforms;
var tmpDir = os.tmpdir();
var configData, buildPlatforms, images, generateQueue, sourceFiles, generatingImages;
var generateLandscape, generatePortrait, orientation;
IonicResources.prototype.run = function() {
buildPlatforms = [];
images = [];
generateQueue = [];
sourceFiles = {};
generatingImages = {};
generateLandscape = false;
generatePortrait = false;
orientation = 'default';
if (!fs.existsSync(settings.configFile)) {
console.error('Invalid ' + settings.configFile + ' file. Make sure the working directory is a Cordova project.');
return;
}
if(argv.default) {
if(hasExistingResources() && !argv.force) {
console.log('The resources folder already exists. We will not overwrite your files unless you pass the --force argument.'.red.bold);
console.log('Running with the force flag will overwrite your resources directory and modify your config.xml file'.red.bold)
return;
}
copyIconFilesIntoResources(argv.force)
.then(function() {
return addIonicIcons('ios')
})
.then(function() {
return addIonicIcons('android');
})
.catch(function(error) {
console.error('An error with adding default resources happened:', error);
})
return
}
if (!fs.existsSync(settings.resourceDir)) {
fs.mkdirSync(settings.resourceDir);
}
var hasPlatforms = true;
if (_.contains(argv._, 'ios')) {
buildPlatforms.push('ios');
} else if (_.contains(argv._, 'android')) {
buildPlatforms.push('android');
} else if (_.contains(argv._, 'wp8')) {
buildPlatforms.push('wp8');
} else if (!fs.existsSync('platforms')) {
hasPlatforms = false;
} else {
buildPlatforms = fs.readdirSync('platforms');
hasPlatforms = buildPlatforms.length;
}
if (!hasPlatforms) {
console.error('No platforms have been added.');
console.error('Please add a platform, for example: ionic platform add ios');
console.error('Or provide a platform name, for example: ionic resources android');
return;
}
getConfigData().then(function() {
if (argv.landscape || argv.l) {
generateLandscape = true;
}
if (argv.portrait || argv.p) {
generatePortrait = true;
}
if (!argv.landscape && !argv.l && !argv.portrait && !argv.p) {
orientation = getOrientationConfigData();
if (orientation == 'landscape') {
generateLandscape = true;
} else if (orientation == 'portrait') {
generatePortrait = true;
} else {
generateLandscape = true;
generatePortrait = true;
}
}
if (argv['api']) {
settings.apiUrl = argv['api'];
}
var promises = [];
if (argv.icon || argv.i) {
console.info('Ionic icon resources generator');
promises.push(queueResTypeImages('icon'));
} else if (argv.splash || argv.s) {
console.info('Ionic splash screen resources generator');
promises.push(queueResTypeImages('splash'));
} else {
console.info('Ionic icon and splash screen resources generator');
promises.push(queueResTypeImages('icon'));
promises.push(queueResTypeImages('splash'));
}
Q.all(promises)
.then(loadSourceImages)
.then(generateResourceImages)
.then(loadResourceImages)
.then(updateConfigData)
.catch(console.error);
IonicStats.t();
});
};
function queueResTypeImages(resType) {
var resTypePlatforms = {};
return buildImagesData()
.then(validateSourceImages)
.then(queueResourceImages)
.catch(console.error);
function buildImagesData() {
var deferred = Q.defer();
buildPlatforms.forEach(function(platform) {
if (!platformConfigs[platform]) return;
var platformResourceDir = path.join(settings.resourceDir, platform);
var resTypeDir = path.join(platformResourceDir, settings[resType + 'Dir']);
if (!fs.existsSync(platformResourceDir)) {
fs.mkdirSync(platformResourceDir);
}
if (!fs.existsSync(resTypeDir)) {
fs.mkdirSync(resTypeDir);
}
_.forEach(platformConfigs[platform][resType].images, function(image) {
var data = _.clone(image);
_.extend(data, {
platform: platform,
src: path.join(resTypeDir, image.name),
nodeName: platformConfigs[platform][resType].nodeName,
nodeAttributes: platformConfigs[platform][resType].nodeAttributes,
resType: resType
});
images.push(data);
});
});
deferred.resolve();
return deferred.promise;
}
function validateSourceImages() {
var deferred = Q.defer();
var validSourceFiles = _.map(settings.sourceExtensions, function(ext) {
return settings[resType + 'SourceFile'] + '.' + ext;
});
images.forEach(function(image) {
if (resTypePlatforms[image.platform]) return;
resTypePlatforms[image.platform] = { platform: image.platform };
});
_.each(resTypePlatforms, function(resTypePlatform) {
for (var x = 0; x < validSourceFiles.length; x++) {
globalSourceFile = path.join(settings.resourceDir, validSourceFiles[x]);
platformSourceFile = path.join(settings.resourceDir, resTypePlatform.platform, validSourceFiles[x]);
if (fs.existsSync(platformSourceFile)) {
resTypePlatform.sourceFilePath = platformSourceFile;
resTypePlatform.sourceFilename = resTypePlatform.platform + '/' + validSourceFiles[x];
break;
} else if (fs.existsSync(globalSourceFile)) {
resTypePlatform.sourceFilePath = globalSourceFile;
resTypePlatform.sourceFilename = validSourceFiles[x];
break;
}
}
if (!resTypePlatform.sourceFilePath || sourceFiles[resTypePlatform.sourceFilePath]) return;
sourceFiles[resTypePlatform.sourceFilePath] = {
filePath: resTypePlatform.sourceFilePath,
filename: resTypePlatform.sourceFilename
};
});
var missingPlatformSources = _.filter(resTypePlatforms, function(resTypePlatform) {
return !resTypePlatform.sourceFilePath;
});
if (missingPlatformSources.length) {
var notFoundDirs = ['resources'];
missingPlatformSources.forEach(function(missingPlatformSource) {
notFoundDirs.push('resources/' + missingPlatformSource.platform);
});
var msg = resType + ' source file not found in ';
if (notFoundDirs.length > 1) {
msg += 'any of these directories: ' + notFoundDirs.join(', ');
} else {
msg += 'the resources directory';
}
console.error(msg);
console.error('valid ' + resType + ' source files: ' + validSourceFiles.join(', '));
}
deferred.resolve();
return deferred.promise;
}
function queueResourceImages() {
var promises = [];
_.each(resTypePlatforms, function(resTypePlatform) {
if (!resTypePlatform.sourceFilePath) return;
var deferred = Q.defer();
promises.push(deferred.promise);
fs.readFile(resTypePlatform.sourceFilePath, function(err, buf) {
if (err) {
deferred.reject('Error reading ' + resTypePlatform.sourceFilePath);
} else {
try {
sourceFiles[resTypePlatform.sourceFilePath].imageId = crc.crc32(buf).toString(16);
var resImages = _.filter(images, function(image) {
return image.resType == resType;
});
resImages.forEach(function(image) {
if (image.platform == resTypePlatform.platform) {
image.sourceFilePath = resTypePlatform.sourceFilePath;
var sourceFile = sourceFiles[image.sourceFilePath];
var tmpFilename = sourceFile.imageId + '-' + image.width + 'x' + image.height + '.png';
image.imageId = sourceFile.imageId;
image.tmpPath = path.join(tmpDir, tmpFilename);
if (resType == 'splash') {
if (!((image.width >= image.height && generateLandscape) || (image.height >= image.width && generatePortrait))) {
image.skip = true;
return;
}
}
if (settings.cacheImages && fs.existsSync(image.tmpPath)) {
console.success(image.resType + ' ' + image.platform + ' ' + image.name + ' (' + image.width + 'x' + image.height + ') from cache');
} else {
loadCachedSourceImageData(sourceFile);
if (sourceFile.cachedData && !sourceFile.cachedData.vector && (sourceFile.cachedData.width < image.width || sourceFile.cachedData.height < image.height)) {
image.skip = true;
console.error(image.resType + ' ' + image.platform + ' ' + image.name + ' (' + image.width + 'x' + image.height + ') skipped, source image ' + sourceFile.filename + ' (' + sourceFile.cachedData.width + 'x' + sourceFile.cachedData.height + ') too small');
} else {
sourceFile.upload = true;
generateQueue.push(image);
}
}
}
});
deferred.resolve();
} catch (e) {
deferred.reject('Error loading ' + resTypePlatform.sourceFilePath + ' md5: ' + e);
}
}
});
});
return Q.all(promises);
}
}
function loadSourceImages() {
var promises = [];
_.each(sourceFiles, function(sourceFile) {
if (!sourceFile.upload) return;
var deferred = Q.defer();
console.log(' uploading ' + sourceFile.filename + '...');
var postData = {
url: settings.apiUrl + settings.apiUploadPath,
formData: {
image_id: sourceFile.imageId,
src: fs.createReadStream(sourceFile.filePath),
cli_version: moduleSettings.version
},
proxy: process.env.PROXY || null
};
request.post(postData, function(err, httpResponse, body) {
function reject(msg) {
try {
msg += JSON.parse(body).Error;
} catch (e) {
msg += body || '';
}
deferred.reject(msg);
}
if (err) {
var msg = 'Failed to upload source image: ';
if (err.code == 'ENOTFOUND') {
msg += 'requires network connection';
} else {
msg += err;
}
deferred.reject(msg);
} else if (!httpResponse) {
reject('Invalid http response');
} else if (httpResponse.statusCode >= 500) {
reject('Image server temporarily unavailable: ');
} else if (httpResponse.statusCode == 404) {
reject('Image server unavailable: ');
} else if (httpResponse.statusCode > 200) {
reject('Invalid upload: ');
} else {
try {
var d = JSON.parse(body);
sourceFile.width = d.Width;
sourceFile.height = d.Height;
sourceFile.vector = d.Vector;
if (sourceFile.vector) {
console.success(sourceFile.filename + ' (vector image) upload complete');
} else {
console.success(sourceFile.filename + ' (' + d.Width + 'x' + d.Height + ') upload complete');
}
cacheSourceImageData(sourceFile);
deferred.resolve();
} catch (e) {
reject('Error parsing upload response: ');
}
}
});
promises.push(deferred.promise);
});
return Q.all(promises);
}
function cacheSourceImageData(sourceFile) {
try {
var data = JSON.stringify({
width: sourceFile.width,
height: sourceFile.height,
vector: sourceFile.vector,
version: 1
});
var cachedImagePath = path.join(tmpDir, sourceFile.imageId + '.json');
fs.writeFile(cachedImagePath, data, function(err) {
if (err) console.error('Error writing cacheSourceImageData: ' + err);
});
} catch (e) {
console.error('Error cacheSourceImageData: ' + e);
}
}
function loadCachedSourceImageData(sourceFile) {
if (settings.cacheImages && !sourceFile.cachedData && sourceFile.cachedData !== false) {
try {
var cachedImagePath = path.join(tmpDir, sourceFile.imageId + '.json');
sourceFile.cachedData = JSON.parse(fs.readFileSync(cachedImagePath));
} catch (e) {
sourceFile.cachedData = false;
}
}
}
function generateResourceImages() {
var deferred = Q.defer();
// https://github.com/ferentchak/QThrottle
var max = settings.generateThrottle - 1;
var outstanding = 0;
function catchingFunction(value) {
deferred.notify(value);
outstanding--;
if (generateQueue.length) {
outstanding++;
generateResourceImage(generateQueue.pop())
.then(catchingFunction)
.fail(deferred.reject);
} else if (outstanding === 0) {
deferred.resolve();
}
}
if (generateQueue.length) {
while (max-- && generateQueue.length) {
generateResourceImage(generateQueue.pop())
.then(catchingFunction)
.fail(deferred.reject);
outstanding++;
}
} else {
deferred.resolve();
}
return deferred.promise;
}
function generateResourceImage(image) {
var deferred = Q.defer();
var sourceFile = sourceFiles[image.sourceFilePath];
if (!sourceFile.vector && (sourceFile.width < image.width || sourceFile.height < image.height)) {
image.skip = true;
console.error(image.resType + ' ' + image.platform + ' ' + image.name + ' (' + image.width + 'x' + image.height + ') skipped, source image ' + sourceFile.filename + ' (' + sourceFile.width + 'x' + sourceFile.height + ') too small');
deferred.resolve();
} else if (generatingImages[image.tmpPath]) {
console.success(image.resType + ' ' + image.platform + ' ' + image.name + ' (' + image.width + 'x' + image.height + ') generated');
deferred.resolve();
} else {
console.log(' generating ' + image.resType + ' ' + image.platform + ' ' + image.name + ' (' + image.width + 'x' + image.height + ')...');
generatingImages[image.tmpPath] = true;
var postData = {
url: settings.apiUrl + settings.apiTransformPath,
formData: {
image_id: image.imageId,
name: image.name,
platform: image.platform,
width: image.width,
height: image.height,
res_type: image.resType,
crop: 'center',
encoding: 'png',
cli_version: moduleSettings.version
},
proxy: process.env.PROXY || null
};
var wr = fs.createWriteStream(image.tmpPath, { flags: 'w' });
wr.on("error", function(err) {
console.error('Error copying to ' + image.tmpPath + ': ' + err);
deferred.resolve();
});
wr.on("finish", function() {
if (!image.skip) {
console.success(image.resType + ' ' + image.platform + ' ' + image.name + ' (' + image.width + 'x' + image.height + ') generated');
deferred.resolve();
}
});
request.post(postData, function(err, httpResponse, body) {
function reject(msg) {
image.skip = true;
try {
delete generatingImages[image.tmpPath];
wr.close();
fs.unlink(image.tmpPath);
} catch (err) {}
try {
msg += JSON.parse(body).Error;
} catch (e) {
msg += body || '';
}
deferred.reject(msg);
}
if (err || !httpResponse) {
reject('Failed to generate image: ' + err);
} else if (httpResponse.statusCode >= 500) {
reject('Image transformation server temporarily unavailable: ');
} else if (httpResponse.statusCode > 200) {
reject('Invalid transformation: ');
}
})
.pipe(wr);
}
return deferred.promise;
}
function loadResourceImages() {
var promises = [];
images.forEach(function(image) {
if (!image.tmpPath || !fs.existsSync(image.tmpPath)) return;
var deferred = Q.defer();
promises.push(deferred.promise);
var rd = fs.createReadStream(image.tmpPath);
rd.on('error', function(err) {
deferred.reject('Unable to read generated image: ' + err);
});
var wr = fs.createWriteStream(image.src, {flags: 'w'});
wr.on('error', function(err) {
deferred.reject('Unable to copy to ' + image.src + ': ' + err);
});
wr.on('finish', function() {
image.isValid = true;
deferred.resolve();
});
rd.pipe(wr);
});
return Q.all(promises);
}
function getConfigData() {
var deferred = Q.defer();
try {
fs.readFile(resSettings.ResSettings.configFile, onConfigRead);
} catch (err) {
console.error('Error saveConfigData: ' + err);
deferred.reject();
}
function onConfigRead(err, data) {
if (err) {
console.error('Error reading config file: ' + err);
deferred.reject();
return;
}
try {
var parser = new xml2js.Parser();
parser.parseString(data, onXmlParse);
} catch (e) {
console.error('Error xml2js parseString: ' + e);
deferred.reject();
}
}
function onXmlParse(err, parsedData) {
if (err) {
console.error('Error parsing config file: ' + err);
deferred.reject();
return;
}
configData = parsedData;
deferred.resolve(configData);
}
return deferred.promise;
}
function updateConfigData() {
images = _.filter(images, function(image) {
return image.isValid && !image.skip;
});
if (!images.length || !configData) return;
var settings = resSettings.ResSettings;
var madeChanges = false;
clearResourcesNodes();
images.forEach(updateImageNode);
buildDefaultIconNode();
buildPreferenceSplashNodes();
writeConfigData(configData);
function clearResourcesNodes() {
images.forEach(function(image) {
if (!image) return;
var platformConfigData = getPlatformConfigData(image.platform);
if (platformConfigData && platformConfigData[image.resType]) {
delete platformConfigData[image.resType];
madeChanges = true;
}
});
}
function updateImageNode(image) {
if (!image) return;
if (!configData.widget.platform) {
configData.widget.platform = [];
}
var platformConfigData = getPlatformConfigData(image.platform);
if (!platformConfigData) {
configData.widget.platform.push({ '$': { name: image.platform } });
platformConfigData = getPlatformConfigData(image.platform);
}
if (!platformConfigData[image.nodeName]) {
platformConfigData[image.nodeName] = [];
}
var node = getResourceConfigNode(platformConfigData, image.nodeName, image.src);
if (!node) {
node = { '$': {} };
platformConfigData[image.nodeName].push(node);
madeChanges = true;
}
image.nodeAttributes.forEach(function(nodeAttribute) {
node.$[nodeAttribute] = image[nodeAttribute];
});
}
function buildDefaultIconNode() {
var currentSize = 0;
var defaultIcon;
images.forEach(function(image) {
if (image && image.resType == 'icon' && image.width > currentSize && image.width <= settings.defaultMaxIconSize) {
currentSize = image.width;
defaultIcon = image;
}
});
if (defaultIcon) {
configData.widget.icon = [{ '$': { src: defaultIcon.src } }];
madeChanges = true;
}
}
function buildPreferenceSplashNodes() {
var hasAndroidSplash = _.find(images, function(image) {
return image.platform == 'android' && image.resType == 'splash';
});
if (hasAndroidSplash) {
if (!configData.widget.preference) {
configData.widget.preference = [];
}
var hasScreenPref = _.find(configData.widget.preference, function(d) {
return d && d.$ && d.$.name == 'SplashScreen';
});
if (!hasScreenPref) {
configData.widget.preference.push({ '$': { name: 'SplashScreen', value: 'screen' } });
madeChanges = true;
}
var hasDelayPref = _.find(configData.widget.preference, function(d) {
return d && d.$ && d.$.name == 'SplashScreenDelay';
});
if (!hasDelayPref) {
configData.widget.preference.push({ '$': { name: 'SplashScreenDelay', value: '3000' } });
madeChanges = true;
}
}
}
}
function getOrientationConfigData(platform) {
if (configData.widget && configData.widget.preference) {
var n = _.find(configData.widget.preference, function(d) {
return d && d.$ && d.$.name && d.$.name.toLowerCase() == 'orientation';
});
if (n && n.$ && n.$.value) {
return n.$.value.toLowerCase();
}
}
return 'default';
}
function getPlatformConfigData(platform) {
if (configData.widget && configData.widget.platform) {
return _.find(configData.widget.platform, function(d) {
return d && d.$ && d.$.name == platform;
});
}
}
function getResourceConfigNode(platformData, nodeName, src) {
if (platformData[nodeName]) {
return _.find(platformData[nodeName], function(d) {
return d && d.$ && d.$.src == src;
});
}
}
function writeConfigData(configData) {
// if (!madeChanges) return;
try {
var builder = new xml2js.Builder();
var xmlString = builder.buildObject(configData);
fs.writeFile(settings.configFile, xmlString, function(err) {
if (err) console.error('Error writing config data: ' + err);
});
} catch (e) {
console.error('Error writeConfigData: ' + e);
}
}
function addIonicIcons(platform, forceAddResources) {
switch(platform) {
case 'android':
case 'ios':
break;
default:
platform = 'android';//most likely crosswalk `cordova platform add ./engine/cordova-android`
}
try {
var promise = null;
if(forceAddResources) {
console.log('forceAddResources')
promise = copyIconFilesIntoResources(forceAddResources);
} else {
promise = Q();
}
return promise
.then(getConfigData)
.then(function(data) {
if(!data.widget.platform || (data.widget.platform.length == 1 && data.widget.platform[0]['$'].name != platform)) {
console.log('Adding icons for platform:'.blue.bold, platform.green)
addPlatformIcons(platform, data)
addSplashScreenPreferences(data)
writeConfigData(data)
}
})
.catch(function(error) {
console.log('Error happened:', error)
throw error;
})
} catch(ex) {
Utils.fail(['Ionic CLI failed to add default Ionic resources - error', ex].join(''));
}
}
function hasExistingResources() {
//Check resources
// resources/icon.png
// resources/splash.png
// resources/ios/
// resources/android
var hasResources = fs.existsSync(path.join('./', 'resources')),
hasAndroidResources = fs.existsSync(path.join('./', 'resources', 'android')),
hasIosResources = fs.existsSync(path.join('./', 'resources', 'ios')),
hasIconResource = fs.existsSync(path.join('./', 'resources', 'icon.png')),
hasSplashResource = fs.existsSync(path.join('./', 'resources', 'splash.png'));
var hasAnyResources = hasResources || hasAndroidResources || hasIosResources || hasIconResource || hasSplashResource;
return hasAnyResources;
}
function copyIconFilesIntoResources(forceAddResources) {
var unzipPath = path.join('resources');
if(hasExistingResources() && !forceAddResources) {
// console.log('Not copying over default resources, the resources directory already exist. Please pass the --force argument to overwrite that folder'.red.bold);
return Q();
}
var ionicResourcesUrl = 'https://github.com/driftyco/ionic-default-resources/archive/master.zip';
// console.log('uzip to: ', unzipPath)
console.log('Downloading Default Ionic Resources'.yellow);
return Utils.fetchArchive(unzipPath, ionicResourcesUrl)
.then(function() {
shelljs.config.silent = true
shelljs.mv('resources/ionic-default-resources-master/*', 'resources')
shelljs.rm('-rf', 'resources/ionic-default-resources-master')
console.log('Done adding default Ionic resources'.yellow)
})
}
function addPlatformIcons(platform, configData) {
var platElem = { '$': { name: platform }, icon: [], splash: [] };
if(!configData.widget.platform) {
configData.widget.platform = [];
}
configData.widget.platform.push(platElem);
resSettings.ResPlatforms[platform].icon.images.forEach(function(image) {
var iconDir = ['resources/', platform, '/icon/', image.name].join('');
if(platform == 'android') {
platElem.icon.push({'$': { src: iconDir, density: image.density }})
} else {
platElem.icon.push({'$': { src: iconDir, width: image.width, height: image.height }})
}
});
resSettings.ResPlatforms[platform].splash.images.forEach(function(image) {
//resources/ios/splash/
var splashDir = ['resources/', platform, '/splash/', image.name].join('');
if(platform == 'android') {
platElem.splash.push({'$': { src: splashDir, density: image.density }})
} else {
platElem.splash.push({'$': { src: splashDir, height: image.height, width: image.width }})
}
})
// generate.writeConfigData(configData);
}
function addSplashScreenPreferences(configData) {
//Check for splash screen stuff
//<preference name="SplashScreen" value="screen"/>
//<preference name="SplashScreenDelay" value="3000"/>
var preferences = configData.widget.preference;
if(!configData.widget.preference) {
configData.widget.preference = []
}
var hasSplashScreen = false, hasSplashScreenDelay = false;
configData.widget.preference.forEach(function(pref) {
if(pref.$.name == 'SplashScreen') {
hasSplashScreen = true;
}
if(pref.$.name == 'SplashScreenDelay') {
hasSplashScreenDelay = true;
}
})
if(!hasSplashScreen) {
configData.widget.preference.push({'$': { name: 'SplashScreen', value: 'screen'}});
}
if(!hasSplashScreenDelay) {
configData.widget.preference.push({'$': { name: 'SplashScreenDelay', value: '3000'}});
}
}
module.exports = {
IonicTask: IonicResources,
addIonicIcons: addIonicIcons,
copyIconFilesIntoResources: copyIconFilesIntoResources,
getConfigData: getConfigData,
hasExistingResources: hasExistingResources,
writeConfigData: writeConfigData
}