UNPKG

emulator-index

Version:

Handles emulator index releated operations

572 lines (514 loc) 20.2 kB
if (!String.prototype.endsWith) { String.prototype.endsWith = function(searchString, position) { var subjectString = this.toString(); if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) { position = subjectString.length; } position -= searchString.length; var lastIndex = subjectString.indexOf(searchString, position); return lastIndex !== -1 && lastIndex === position; }; } var fs = require("fs"); var path = require("path"); var uuid = require('node-uuid'); var CRC32 = require("crc-32"); var walk = require('walk'); var DOMParser = require('xmldom').DOMParser; var xpath = require('xpath'); var reAsset = new RegExp("Assets\\" + path.sep + "\\w+\\.(?:imageset|appiconset|launchimage)\\" + path.sep); var _contentsJSONCache = {}; /** * This callback is provied as function to getProjectID as a parameter * @callback getProjectIDCallback * @param {string} projectID - Retrieved from project.json */ /** * Gets project unique identifier from project.json in a Smartface workspace * @param {boolean} withSave - Optional. If projectID is missing from project.json, creates a new one, saves to project.json and retrieves it. Otherwise if missing it will throw error. * @param {getProjectIDCallback} callback - Optional. Asynch call function as parameter. If provided, all operations will be synch. * @returns {undefined} - If callback parameter is provided will return undefined, otherwise will return projectID * @throws If project.json does not have projectID and withSave parameter is provided as false or not provided at all */ function getProjectID(withSave, callback) { if (typeof callback === "undefined" && typeof withSave === "function") { return getProjectID(false, withSave); } withSave = Boolean(withSave); var me = this; if (this.projectID) { if (callback) return callback(this.projectID); else return this.projectID; } if (callback) { fs.readFile(this.projectJSONPath, "utf8", parseProjectJSONForID); } else { try { var data = fs.readFileSync(this.projectJSONPath, "utf8"); return parseProjectJSONForID(null, data); } catch (ex) { return parseProjectJSONForID(ex, null); } } var project = {}; function parseProjectJSONForID(err, data) { if (err) throw err; project = JSON.parse(data); if (!project.projectID) { if (!withSave) { throw Error("projectID is missing in project.json." + "\nPlease update project.json file with a valid id or call this " + "method withSave option"); } else { project.projectID = uuid.v4(); data = JSON.stringify(project, null, 4); if (callback) { fs.writeFile(me.projectJSONPath, data, "utf8", retrieveData); } else { try { fs.writeFileSync(me.projectJSONPath, data, "utf8"); return retrieveData(null); } catch (ex) { return retrieveData(err); } } } } else { return retrieveData(null); } } function retrieveData(err) { if (err) throw err; if (callback) { callback.call(me, project.projectID); } else { return project.projectID; } } } /** * Device info provided as device options * @typedef {Object} Device * @property {string} deviceID - Random GUID generated by Emulator once * @property {string} deviceName - Name of the Device * @property {string} brandName - Name of the device manifacturer * @property {string} os - Operating System name of the device (iOS | Android) * @property {string} osVersion - OS version of the device * @property {string} smartfaceVersion - Smartface runtime version such as: 4.4.0.1 * @property {Object} screen - Device screen info * @proerty {Object} screen.px - Sceen pixel values * @proerty {number} screen.px.height - Sceen height pixel value * @proerty {number} screen.px.width - Sceen width pixel value * @proerty {Object} screen.dp - Sceen dp values * @proerty {number} screen.dp.height - Sceen height dp value * @proerty {number} screen.dp.width - Sceen width dp value * @proerty {Object} screen.pt - Sceen pt values * @proerty {number} screen.pt.height - Sceen height pt value * @proerty {number} screen.pt.width - Sceen width pt value * @proerty {Array.string} resourceFolderOrder - Density based resource folder names given in order */ /** * Emulator Project Index * @typedef {Object} Index * @property {************** */ /** * @callback GetIndexCallback * @param {Index} data - Device index provided as data property of the callback */ /** * Calculates index from workspace * @param {Device} device - Required. Device information to get correct image resources * @param {GetIndexCallback} - Required. When provided performs asynch operation */ function getIndex(device, callback) { var projectJSON = fs.readFileSync(this.projectJSONPath, "utf8"); projectJSON = JSON.parse(projectJSON); _contentsJSONCache = {}; //needs reset var me = this; var taskCount = 4; var index = { projectID: this.getProjectID(true), info: projectJSON.info, files: {} }; walkFolder(this.assetsPath, function walk_callback_assets(files) { processFolder(index, files, "asset", done); }); walkFolder(this.scriptsPath, function walk_callback_scripts(files) { processFolder(index, files, "script", done); }); fs.stat(this.fontConfigPath, function(err, stat) { if (err) { console.log("There is an error while processing FontConfig.xml file"); return; } fs.readFile(me.fontConfigPath, "utf8", function fileContentFontConfigXML(err, data) { if (err) throw err; var doc = new DOMParser().parseFromString(data); var query = "/Fonts/Font/@Name"; if (device.os === "Android") query = "/Fonts/Font/@Name[../@AndroidPublish = 'Y']"; else if (device.os === "iOS") { query = "/Fonts/Font/@Name[../@iOSPublish = 'Y']"; } var nodes = xpath.select(query, doc); nodes.forEach(function(element, idx, array) { var regexSpace = / /g; var fontName = element.value.replace(regexSpace, "."); taskCount++; var fontFolder = path.join(path.dirname(me.fontConfigPath), "Fonts", fontName); walkFolder(fontFolder, function walk_callback_fonts(files) { processFolder(index, files, "font", done); }); }); done(); }); }); var otherMapping = [{ path: path.join(this.configPath, "defaults.xml"), scheme: "config", relativeTo: this.configPath }]; function handleOther() { var handled = []; var mapping; for (var i = 0; i < otherMapping.length; i++) { mapping = otherMapping[i]; if(mapping.os && (mapping.os !== device.os)) { handled.push(i); continue; } fs.stat(mapping.path, function otherStatCallback(err, stats) { if(err) return; taskCount++; var fileObject = {}; fileObject[mapping.path] = path.relative(mapping.relativeTo, mapping.path); processFolder(index, fileObject, mapping.scheme, done); }); } } handleOther(); function handleImages() { if (device.os === "iOS") { handleImages_iOS(); } else if (device.os === "Android") { handleImages_Android(); } } handleImages(); function handleImages_iOS() { var order = [2, 3, 1]; if (device.brandName.indexOf("Plus") > -1) { order = [3, 2, 1]; } else { var nonRetinaDevices = ["iPhone", "iPhone 2G", "iPhone 3G", "iPhone 3GS", "iPod Touch (1 Gen)", "iPod Touch (2 Gen)", "iPod Touch (3 Gen)", "iPad", "iPad 3G", "iPad 2", "iPad Mini" ]; if (nonRetinaDevices.indexOf(device.brandName) > -1) { order = [1, 2, 3]; } } var iOSImagesFolder = path.join(me.imagesPath, "iOS"); walkFolder(iOSImagesFolder, function walk_callback_iOSImages(files) { var filesArray = Object.keys(files); var images = {}; var newFiles = {}; filesArray.forEach(function(element, idx, array) { var fileInfo = path.parse(element); if (fileInfo.base === "Contents.json") return; //skip Contents.json var imgInfo = getiOSImageInfo(element); imgInfo.fullPath = element; imgInfo.priority = order.indexOf(imgInfo.multiplier); if (!images[imgInfo.name]) images[imgInfo.name] = imgInfo; else { var other = images[imgInfo.name]; if (other.priority > imgInfo.priority) images[imgInfo.name] = imgInfo; } }); for (var imgInfoName in images) { newFiles[images[imgInfoName].fullPath] = files[images[imgInfoName].fullPath]; } Object.defineProperty(newFiles, "__ofBaseFolder", { enumerable: false, configurable: true, value: files.__ofBaseFolder }); processFolder(index, newFiles, "image", done, { os: "iOS" }); }); } function getiOSImageInfo(name) { var fileInfo = path.parse(name); var imgName = fileInfo.name; var multiplier = 1; reAsset.lastIndex = 0; //requires reset before any reuse if (reAsset.test(name)) { //is an asset image var contents = getContentsJSON(name); var imageRecord; var assetName = path.parse(path.dirname(name)).name; for (var i = 0; i < contents.images.length; i++) { imageRecord = contents.images[i]; if (imageRecord.filename === fileInfo.base) { multiplier = Number(imageRecord.scale[0]); var ret = { multiplier: multiplier, name: assetName, assetName: assetName }; return ret; } } return { name: "", multiplier: Number.MIN_VALUE } } else { //is not an asset image if (imgName.endsWith("@2x")) multiplier = 2; else if (imgName.endsWith("@3x")) multiplier = 3; switch (multiplier) { case 1: return { name: imgName, multiplier: 1 }; case 2: case 3: return { name: imgName.substr(0, name.length - 3), multiplier: multiplier }; default: throw Error("unhandeled image naming for iOS"); } } } function getContentsJSON(name) { var contentsJSONPath = path.join(path.dirname(name), "Contents.json"); if (!_contentsJSONCache[contentsJSONPath]) { _contentsJSONCache[contentsJSONPath] = JSON.parse( fs.readFileSync(contentsJSONPath, "utf8")); } return _contentsJSONCache[contentsJSONPath]; } function handleImages_Android() { var androidImagesFolder = path.join(me.imagesPath, "Android"); walkFolder(androidImagesFolder, function walk_callback_AndroidImages(files) { var filesArray = Object.keys(files).filter(function(value) { return path.relative(androidImagesFolder, value).split(path.sep).length === 2; }, filesArray); var images = {}; var newFiles = {}; filesArray.forEach(function(element, idx, array) { var imgInfo = getAndroidImageInfo(element); imgInfo.fullPath = element; if (!images[imgInfo.name]) images[imgInfo.name] = imgInfo; else { var other = images[imgInfo.name]; if (other.priority > imgInfo.priority) images[imgInfo.name] = imgInfo; } }); for (var imgInfo in images) { newFiles[images[imgInfo].fullPath] = path.parse(files[images[imgInfo].fullPath]).base; } Object.defineProperty(newFiles, "__ofBaseFolder", { enumerable: false, configurable: true, value: files.__ofBaseFolder }); processFolder(index, newFiles, "image", done, { os: "Android" }); }); } function getAndroidImageInfo(fullPath) { var fileInfo = path.parse(fullPath); var density = path.parse(path.dirname(fullPath)).name; var priority = device.resourceFolderOrder.indexOf(density); return { name: fileInfo.name, density: density, fullPath: fullPath, priority: priority === -1 ? Number.MAX_VALUE : priority }; } function done() { taskCount--; if (taskCount !== 0) return; index = sort(index); callback(index); } } function sort(obj) { if (typeof obj !== "object") return obj; var props = Object.keys(obj).sort(); var newObject = {}; var i, p; for (i = 0; i < props.length; i++) { p = props[i]; newObject[p] = sort(obj[p]); } if (props.length === 0) return obj; return newObject; } function walkFolder(folder, callback) { var files = {}; Object.defineProperty(files, "__ofBaseFolder", { enumerable: false, configurable: true, value: folder }); var walker = walk.walk(folder, { followLinks: false }); walker.name = folder; walker.on("file", fileHandler); walker.on("end", endHandler); function fileHandler(root, fileStat, next) { var fullPath = path.join(root, fileStat.name); var relativePath = path.relative(folder, fullPath); relativePath = relativePath.split(path.sep).join("/"); files[fullPath] = relativePath; if (typeof next === "function") next(); } function endHandler() { callback(files); } } function processFolder(index, files, schema, callback, options) { options = options || {}; index = index || {}; index.files = index.files || {}; var taskCount = 0, me = this, filesArray = Object.keys(files); for (var i = 0; i < filesArray.length; i++) { taskCount += 2; getFileStats(filesArray[i]); getCRC(filesArray[i]); } function finalize() { if (taskCount !== 0) return; callback.call(me, index); } function getFileStats(file) { fs.stat(file, function fStat(err, stats) { if (err) { throw err; } var uri = getURI(file); index.files[uri] = index.files[uri] || {}; index.files[uri].date = stats.ctime; taskCount--; finalize(); }); } function getCRC(file) { fs.readFile(file, "binary", function(err, data) { if (err) { throw err; } var uri = getURI(file); index.files[uri] = index.files[uri] || {}; index.files[uri].crc = CRC32.buf(data); taskCount--; finalize(); }); } function getURI(file) { reAsset.lastIndex = 0; if (options.os === "Android") { var density = path.parse(path.dirname(file)).name; return schema + "://" + files[file] + "?density=" + density; } else if (options.os === "iOS") { if (schema === "image" && reAsset.test(files[file])) { var fileInfo = path.parse(files[file]); var assetInfo = path.parse(fileInfo.dir); var contentJSONImages = _contentsJSONCache[path.join(path.dirname(file), "Contents.json")].images; for (var i = 0; i < contentJSONImages.length; i++) { if (contentJSONImages[i].filename === fileInfo.base) { return schema + "://" + assetInfo.name + (contentJSONImages[i].scale === "1x" ? "" : "@" + contentJSONImages[i].scale) + fileInfo.ext + "?path=" + encodeURIComponent(files[file]); } } throw Error("No file found in Xcode asset content.json"); } else { } } return schema + "://" + files[file]; } } /** * Workspace constructor options parameter object * @typedef {Object} workspaceOptions * @property {string} path - Path of the workspace. * @property {string} projectJSONPath - Path of the project.json file relative to workspace. Defaults to path.join(options.path, "config", "project.json") * @property {string} scriptsPath - Path of the scripts folder relative to workspace. Defaults to path.join(options.path, "scripts") * @property {string} imagesPath - Path of the scripts folder relative to workspace. Defaults to path.join(options.path, "images") * @property {string} assetsPath - Path of the scripts folder relative to workspace. Defaults to path.join(options.path, "assets") * @property {string} fontConfigPath - Path of the FontConfig.xml file relative to workspace. Defaults to path.join(options.path, "config", "FontConfig.xml") * @property {string} projectID - Optinal. Unique Identifier of ProjectID. If not provided a UUID.v4 will be added to project.json and it will be used. Otherwise provided identifier will be used */ /** * Creates a new workspace instance with options * @class * @param {workspaceOptions} options - Required. Creates a workspace with required options */ function Workspace(options) { if (!(this instanceof Workspace)) return new Workspace(options); if (!options) { throw Error("Options are required"); } /** @type {string} */ this.path = options.path || "/home/ubuntu/workspace/"; /** @type {string} */ this.projectJSONPath = path.join(this.path, options.projectJSONPath || path.join("config", "project.json")); /** @type {string} */ this.scriptsPath = path.join(this.path, options.scriptsPath || "scripts"); /** @type {string} */ this.imagesPath = path.join(this.path, options.imagesPath || "images"); /** @type {string} */ this.assetsPath = path.join(this.path, options.assetsPath || "assets"); /** @type {string} */ this.configPath = path.join(this.path, options.configPath || "config"); /** @type {string} */ this.fontConfigPath = path.join(this.path, options.fontConfigPath || path.join("config", "FontConfig.xml")); /** @type {string} */ this.projectID = options.projectID; } Workspace.prototype.getProjectID = getProjectID; Workspace.prototype.getIndex = getIndex; module.exports = Workspace;