UNPKG

monaca-lib

Version:

Monaca cloud and localkit API bindings for JavaScript

1,684 lines (1,505 loc) 138 kB
(function() { 'use strict'; var Q = require('q'), qlimit = require('qlimit'), request = require('request'), os = require('os'), path = require('path'), fs = require('fs-extra'), shell = require('shelljs'), crc32 = require('buffer-crc32'), nconf = require('nconf'), extend = require('extend'), crypto = require('crypto'), xml2js = require('xml2js'), lockfile = require('lockfile'), tmp = require('tmp'), extract = require('extract-zip'), glob = require('glob'), EventEmitter = require('events'), ora = require('ora'), compareVersions = require('compare-versions'), showNpmVersion = false, npm; const { spawn } = require('child_process'); const utils = require(path.join(__dirname, 'utils')); const migration = require('./migration'); const fixPath = require('fix-path'); // support project type const REACT_NATIVE = 'react-native'; const CAPACITOR = 'capacitor'; const CORDOVA = 'cordova'; // Spinner var spinner = null; // local imports var localProperties = require(path.join(__dirname, 'monaca', 'localProperties')); var USER_CORDOVA = path.join( process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'], '.cordova' ); var NPM_PACKAGE_FILE = path.join(USER_CORDOVA, 'package.json'); var USER_DATA_FILE = path.join(USER_CORDOVA, 'monaca.json'); var CONFIG_FILE = path.join(USER_CORDOVA, 'monaca_config.json'); // error message const ERROR_DIRECTORY_PATH_MESSAGE = 'This is directory. Please provide the file path.'; // config var config = nconf.env() .file(path.join(__dirname, 'config.json')) .get('monaca'); /** * @class Monaca * @description * Create Monaca API object. * @param {string} [apiRoot] - Root of Monaca web API. Defaults to {@link https://ide.monaca.mobi/api}. * @example * var monaca = new Monaca(); * * monaca.login('my@email.org', 'mypassword').then( * function() { * // Login successful. Let's do some stuff! * }, * function(error) { * // Login failed! :( * } * ); */ var Monaca = function(apiRoot, options) { // Parameters are optional. if (typeof apiRoot === 'object') { options = apiRoot; apiRoot = undefined; } else { options = options || {}; } var webApiRoot; if (!apiRoot) { try { if (!fs.existsSync(USER_CORDOVA)) fs.mkdirsSync(USER_CORDOVA); if (!fs.existsSync(CONFIG_FILE)) fs.writeFileSync(CONFIG_FILE, '{}'); } catch (err) { console.log('Could not write config file ' + err); } try { var configContent = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); var apiEndpoint = configContent['api_endpoint']; if (apiEndpoint) { webApiRoot = 'https://' + apiEndpoint + '/en/api'; apiRoot = 'https://ide.' + apiEndpoint + '/api'; } } catch (e) { console.log("Cound not find/set the custom API endpoint " + e); } } /** * @description * Root of Monaca IDE API. * @name Monaca#apiRoot * @type string * @default https://ide.monaca.mobi/api */ Object.defineProperty(this, 'apiRoot', { value: apiRoot ? apiRoot : config.default_api_root, writable: true }); /** * @description * Root of Monaca web API. * @name Monaca#webApiRoot * @type string * @default https://monaca.mobi/en/api */ Object.defineProperty(this, 'webApiRoot', { value: webApiRoot ? webApiRoot : config.web_api_root, writable: true }); /** * @description * Executable path of npm. * @name Monaca#npmPath * @type string */ Object.defineProperty(this, 'npmPath', { value: options.npmPath || '', writable: true }); /** * @description * Executable path of node. * @name Monaca#nodePath * @type string */ Object.defineProperty(this, 'nodePath', { value: options.nodePath || '', writable: true }); /** * @description * Version of Monaca library * @name Monaca#version * @type string */ Object.defineProperty(this, 'version', { value: require(path.join(__dirname, '..', 'package.json')).version, writable: false }); /** * @description * Package name. * @name Monaca#packageName * @type string */ Object.defineProperty(this, 'packageName', { value: require(path.join(__dirname, '..', 'package.json')).name, writable: false }); /** * @description * Client type. * @name Monaca#clientType * @type string */ Object.defineProperty(this, 'clientType', { value: options.clientType || 'local', writable: false }); /** * @description * Client version. * @name Monaca#clientVersion * @type string */ Object.defineProperty(this, 'clientVersion', { value: options.clientVersion || '0.0.0', writable: false }); /** * @description * Debug. * @name Monaca#debug * @type boolean */ Object.defineProperty(this, 'debug', { value: (options.hasOwnProperty('debug') && options.debug === true), writable: false }); /** * @description * userCordova. * @name Monaca#userCordova * @type string */ Object.defineProperty(this, 'userCordova', { value: USER_CORDOVA, writable: false }); if (this.clientType === 'localkit') { this._setExecutablePathForNPMAndNode(); } else { // Check node version if (compareVersions(process.version, '10.0.0') < 0) { console.log('We have detected that you are using the old version of Node (' + process.version + '). Please upgrade it to 10.x or higher.'); console.log('In order to work with this version, We need to apply this patch - process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0" \n\r') process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; } } this.tokens = { api: null, session: null }; this.loginBody = null; this._loggedIn = false; if (this.debug) { request.debug = true; } this.projectSettings = null; this.emitter = new EventEmitter(); this._monacaData = this._loadAllData(); }; Monaca.prototype.setAPIConfig = function(apiEndpoint) { var deferred = Q.defer(); try { this.apiRoot = 'https://ide.' + apiEndpoint + '/api'; this.webApiRoot = 'https://' + apiEndpoint + '/en/api'; return this.setConfig('api_endpoint', apiEndpoint) .then( function(result) { deferred.resolve(); } ) .catch( function(e) { deferred.reject(e); } ) } catch (e) { deferred.reject(e); } return deferred.promise; } Monaca.prototype.resetAPIConfig = function() { var deferred = Q.defer(); try { this.apiRoot = config.default_api_root; this.webApiRoot = config.web_api_root; return this.removeConfig('api_endpoint') .then( function(result) { deferred.resolve(); } ) .catch( function(e) { deferred.reject(e); } ) } catch (e) { deferred.reject(e); } return deferred.promise; } Monaca.prototype._generateUUIDv4 = function(a, b) { for ( b = a = ''; a++ < 36; b += a * 51 & 52 ? ( a ^ 15 ? 8 ^ Math.random() * (a ^ 20 ? 16 : 4) : 4 ).toString(16) : '-' ); return b; }; Monaca.prototype.getTrackId = function() { if (this.getData('trackId')) { return Q.resolve(this.getData('trackId')); } return this.setData({ trackId: this._generateUUIDv4() }).then(Q.resolve.bind(null, this.getData('trackId'))); }; /** * @method * @memberof Monaca * @description * Reports any event to Monaca backend. It keeps the previous promise flow. * @param {object} report - Report object. Must contain 'event'. * Optional values are 'arg1' and 'otherArgs'. * @param {any} resolvedValue - Optional. This parameter will be returned by the promise. * @return {Promise} * @example * monaca.reportAnalytics({event: 'create'}) * .then(nextMethod) */ Monaca.prototype.reportAnalytics = function(report, resolvedValue) { return this.getTrackId().then( function(trackId) { var form = extend({}, report, { event: 'monaca_lib_' + report.event }, { trackId: trackId, clientType: this.clientType, version: this.version, clientId: this.getData('clientId') }); return this._post(this.apiRoot + '/user/track', form) .then(Q.resolve.bind(null, resolvedValue), Q.resolve.bind(null, resolvedValue)); }.bind(this), Q.resolve.bind(null, resolvedValue) ); }; /** * @method * @memberof Monaca * @description * Reports a fail event to Monaca backend. This must be used with a rejected promise. * It keeps the rejected promise flow. * @param {object} report - Report object. Must contain 'event'. * Optional values are 'arg1' and 'otherArgs'. * @param {any} resolvedValue - Optional. This parameter will be returned by the promise. * @return {Rejected promise} * @example * monaca.reportFail({event: 'create'}) * .catch(handleErrors) */ Monaca.prototype.reportFail = function(report, error) { report.errorDetail = error === 'object' ? error.message : error; return this.reportAnalytics(extend({}, report, { event: report.event + '_fail' })) .then(Q.reject.bind(null, error)); }; /** * @method * @memberof Monaca * @description * Reports a finish event to Monaca backend. It keeps the previous promise flow. * @param {object} report - Report object. Must contain 'event'. * Optional values are 'arg1' and 'otherArgs'. * @param {any} resolvedValue - Optional. This parameter will be returned by the promise. * @return {Promise} * @example * monaca.reportAnalytics({event: 'create'}) * .then(nextMethod) */ Monaca.prototype.reportFinish = function(report, resolvedValue) { return this.reportAnalytics(extend({}, report, { event: report.event + '_finish' }), resolvedValue); }; Monaca.prototype._safeParse = function(jsonString) { try { return JSON.parse(jsonString); } catch(e) { throw new Error('Not a JSON response.'); } }; Monaca.prototype._loadAllData = function() { var data; try { data = require(USER_DATA_FILE); } catch(e) { if (e.code === 'MODULE_NOT_FOUND') { return {}; } throw e; } return data; }; Monaca.prototype._saveAllData = function() { var deferred = Q.defer(), jsonData; try { jsonData = JSON.stringify(this._monacaData); } catch (error) { return deferred.reject(error); } fs.exists(path.dirname(USER_DATA_FILE), function(exists) { if (!exists) { shell.mkdir('-p', path.dirname(USER_DATA_FILE)); } fs.writeFile(USER_DATA_FILE, jsonData, function(error) { if (error) { deferred.reject(error); } else { deferred.resolve(); } }); }); return deferred.promise; }; Monaca.prototype.setData = function(data) { extend(this._monacaData, data); return this._saveAllData(); }; Monaca.prototype.getData = function(key) { return this._monacaData[key]; }; Monaca.prototype._filterFiles = function(dst, src) { for (var key in dst) { utils.spinnerLoading(spinner, 'Comparing File: ' + key); if (dst.hasOwnProperty(key)) { var d = dst[key]; if (d.type == 'dir') { delete dst[key]; } else if (dst.hasOwnProperty(key) && src.hasOwnProperty(key)) { var s = src[key]; if (d.hash === s.hash) { delete dst[key]; } } } } }; Monaca.prototype._filterMonacaIgnore = function(projectDir) { return this._getMonacaIgnore(projectDir) .split("\r\n") // Split by \r\n. .map(function(ele) { return ele.split("\n")}) // Split each array element from previous step by \n again. .reduce(function(a,b) { return a.concat(b)}) // Now concat them into one array. .filter(function(n) { return n.trim() !== "" && n.indexOf("#") !== 0; }); }; Monaca.prototype._getMonacaIgnore = function(projectDir) { var monacaIgnore = path.resolve(projectDir, '.monacaignore'); if (fs.existsSync(monacaIgnore)) { return fs.readFileSync(monacaIgnore, 'utf8'); } else { // generate .monacaignore for all frameworks var result = this._generateMonacaIgnore(projectDir); if (result instanceof Error) { throw result; } else { return this._getMonacaIgnore(projectDir); } } }; Monaca.prototype._generateMonacaIgnore = function(projectDir) { let defaultConfig = ''; let cordovaVersion = 0; // Get cordova version try { cordovaVersion = parseInt(this.getCordovaVersion(projectDir)); } catch (e) { cordovaVersion = 0; } if (cordovaVersion > 5) { defaultConfig = path.resolve(__dirname, 'default-config', '.monacaignore'); } else { // remove /platform from .monacaignore for lower cordovaVersion <= 5 defaultConfig = path.resolve(__dirname, 'default-config', 'cordova5', '.monacaignore'); } if (fs.existsSync(defaultConfig)) { console.log('Generating default .monacaignore file.'); console.log('This file is used to filter files for download and upload operation. Please modify the content as your requirements.'); return fs.copySync(defaultConfig, path.resolve(projectDir, '.monacaignore')); } else { return (new Error('No default .monacaignore file found.')); } }; Monaca.prototype._createRequestClient = function(data) { var deferred = Q.defer(), qs = {}; var apiToken = this.getData('x-monaca-param-api-token'), session = this.getData('x-monaca-param-session'); if (apiToken) { qs.api_token = apiToken; } if (data) { extend(qs, data); } this.getConfig('http_proxy').then( function(httpProxy) { var requestClient = request.defaults({ qs: qs, // rejectUnauthorized: false, // DELETE encoding: null, proxy: httpProxy, headers: { Cookie: session || null }, timeout: 300 * 1000 }); deferred.resolve(requestClient); }.bind(this), function(error) { deferred.reject(error); } ); return deferred.promise; }; Monaca.prototype._request = function(method, resource, data, requestClient, isFile ) { method = method.toUpperCase(); resource = resource.match(/^https?\:\/\//) ? resource : (this.apiRoot + resource); var deferred = Q.defer(); var createRequestClient = function() { return (requestClient ? Q.resolve(requestClient) : this._createRequestClient(method === 'GET' ? data : undefined)); }.bind(this); createRequestClient().then( function(requestClient) { var hashData = null; if (! isFile) { hashData = { method: method, url: resource, form: method === 'POST' ? data : undefined }; } else { hashData = { method: method, url: resource, formData: method === 'POST' ? data : undefined }; } requestClient( hashData , function(error, response, body) { if (error) { deferred.reject(error.code); } else { const unauthorizedErrorMessage = 'Error in user authentication. Please login first.'; if (response.statusCode === 200) { deferred.resolve({body: body, response: response}); } else if (response.statusCode === 401 && resource.startsWith(this.apiRoot) && !this.retry) { this.retry = true; this.relogin().then(function() { deferred.resolve(this._request(method, resource, data, null )); }.bind(this), function(error) { deferred.reject(new Error(unauthorizedErrorMessage)); }); } else if (response.statusCode === 401) { let errorMessage = unauthorizedErrorMessage; const bodyJson = JSON.parse(body); if (body && bodyJson && bodyJson.message) { errorMessage = bodyJson.message; } deferred.reject(new Error(errorMessage)); } else { try { deferred.reject(JSON.parse(body)); } catch (e) { deferred.reject(response.statusCode); } } } }.bind(this) ); }.bind(this), function(error) { deferred.reject(error); } ) return deferred.promise; }; Monaca.prototype._get = function(resource, data) { return this._request('GET', resource, data); }; Monaca.prototype._post = function(resource, data) { return this._request('POST', resource, data); }; Monaca.prototype._post_file = function(resource, data) { return this._request('POST', resource, data, null, true ); }; /** * @method * @memberof Monaca * @description * Returns information about server availability * @return {Promise} */ Monaca.prototype.getConnectionStatus = function() { var url = this.apiRoot.match(/https(.*)\//)[0] + 'server_check'; return this._post(url, {}) .then( function(res) { if ((/^2/).test(res.response.statusCode)) { return Q.resolve('available'); } else { return Q.resolve('not available'); } }, function() { return Q.resolve('not available'); } ); }; /** * @method * @memberof Monaca * @description * Download project file and save to disk. Must be loggeed in to * use. * @param {string} projectId - Monaca project id. * @param {string} remotePath - Source file in cloud. * @param {string} localPath - Local file destination. * @param {number} retryCount - Retry count to download a file. * @return {Promise} * @example * monaca.downloadFile('SOME_PROJECT_ID', '/remote/file', '/local/file').then( * function() { * // File download successful! * }, * function(error) { * // File download failed. * } * ); */ Monaca.prototype.downloadFile = function(projectId, remotePath, localPath, retryCount = 0) { const RETRY_COUNT_LIMIT = 3; const RETRY_SLEEP_MS = 3000; var deferred = Q.defer(); this._post('/project/' + projectId + '/file/read/' + encodeURIComponent(remotePath), { path: remotePath }).then( function(data) { var parentDir = path.dirname(localPath); fs.exists(parentDir, function(exists) { if (!exists) { shell.mkdir('-p', parentDir); } fs.writeFile(localPath, data.body, function(error) { if (error) { deferred.reject(error); } else { deferred.resolve(localPath); } }); }); }, async function(error) { if (retryCount < RETRY_COUNT_LIMIT) { console.log(`Retrying ${retryCount + 1} times for ${remotePath}...`); await utils.sleep(RETRY_SLEEP_MS); this.downloadFile(projectId, remotePath, localPath, retryCount + 1) .then(deferred.resolve) .catch(deferred.reject); } else { deferred.reject(error); } }.bind(this) ); return deferred.promise; }; /** * @method * @memberof Monaca * @description * Upload a file from disk to the cloud. Must be logged in to use. * @param {string} projectId - Monaca project ID. * @param {string} localPath - Local source file. * @param {string} remotePath - Remote file in cloud. * @return {Promise} * @example * monaca.uploadFile('SOME_PROJECT_ID', '/local/file', '/remote/file').then( * function() { * // File upload successful! * }, * function(error) { * // File upload failed. * } * ); */ Monaca.prototype.uploadFile = function(projectId, localPath, remotePath) { var deferred = Q.defer(); fs.exists(localPath, function(exists) { if (!exists) { deferred.reject(new Error('File does not exist.')); } else { fs.readFile(localPath, function(error, data) { if (error) { deferred.reject(error); } else { this._post_file('/project/' + projectId + '/file/save/' + encodeURIComponent(remotePath), { path: remotePath, // contentBase64: data.toString('base64') file: fs.createReadStream(localPath) }).then( function() { deferred.resolve(remotePath); }, function(error) { deferred.reject(error); } ); } }.bind(this)); } }.bind(this)); return deferred.promise; }; Monaca.prototype._deleteFileFromCloud = function(projectId, remotePath) { var deferred = Q.defer(); this._post('/project/' + projectId + '/file/remove', { paths: remotePath }).then( function() { deferred.resolve(remotePath); }, function(error) { deferred.reject(error); } ); return deferred.promise; }; Monaca.prototype._login = function() { var deferred = Q.defer(), options; if (arguments.length === 3) { options = arguments[2]; } else if (typeof arguments[1] === 'object') { options = arguments[1]; } else { options = {}; } var form = { language: options.language || 'en', clientType: options.clientType || this.clientType || 'local', version: options.version || this.packageName + ' ' + this.version, os: os.platform() }; // Edition option is for Visual Studio only. if (options.edition) { form.edition = options.edition; } if (arguments.length === 1 || typeof arguments[1] === 'object') { form.token = arguments[0]; } else { form.email = arguments[0]; form.password = arguments[1]; } return this._post(this.apiRoot + '/user/login', form) .then( function(data) { var body = this._safeParse(data.body), response = data.response; if (body.status === 'ok') { var headers = response.caseless.dict; return this.setData({ 'reloginToken': body.result.token, 'clientId': body.result.clientId, 'x-monaca-param-api-token': headers['x-monaca-param-api-token'], 'x-monaca-param-session': headers['x-monaca-param-session'], 'subscriberUrl': body.result.subscriberUrl, }) .then( function() { this.tokens = { api: headers['x-monaca-param-api-token'], session: headers['x-monaca-param-session'] }; this.loginBody = body.result; this._loggedIn = true; return Q.resolve(this.loginBody); }.bind(this), Q.reject ); } return Q.reject(body); }.bind(this), Q.reject ); }; /** * @method * @memberof Monaca * @description * Prepares the current session with local data before trying any request. * If local data is not found it calls relogin. * @param {object} [options] - Login parameters. * @param {string} [options.version] - App name and version to send to the Monaca API. Defaults to "monaca-lib x.y.z". * @param {string} [options.language] - Can be either "en" or "ja". Defaults to "en". * @return {Promise} * @example * monaca.prepareSession().then( * function() { * // Login successful! * }, * function(error) { * // Login failed! * } * ); */ Monaca.prototype.prepareSession = function(options) { var apiToken = this.getData('x-monaca-param-api-token'), session = this.getData('x-monaca-param-session'); if (!apiToken || !session) { return this.relogin(options); } this.tokens = { api: apiToken, session: session }; this._loggedIn = true; return Q.resolve(); }; /** * @method * @memberof Monaca * @description * Login to Monaca cloud using a saved relogin token. Use {@link Monaca#login} to * login the first time. * @param {object} [options] - Login parameters. * @param {string} [options.version] - App name and version to send to the Monaca API. Defaults to "monaca-lib x.y.z". * @param {string} [options.language] - Can be either "en" or "ja". Defaults to "en". * @return {Promise} * @example * monaca.relogin().then( * function() { * // Login successful! * }, * function(error) { * // Login failed! * } * ); */ Monaca.prototype.relogin = function(options) { options = options || {}; var reloginToken = this.getData('reloginToken'); if (typeof reloginToken !== 'string' || reloginToken === '') { return Q.reject(new Error('Not a valid relogin token.')); } return this._login(reloginToken, options); }; /** * @method * @memberof Monaca * @description * Sign in to Monaca cloud using email and password. Will save relogin token to disk * if successful. After the relogin token has been saved, {@link Monaca#relogin} can * be used to login. * @param {string} email - A Monaca account email. * @param {string} password - Password associated with the account. * @param {object} [options] - Additional options. * @param {string} [options.version] - App name and version to send to the Monaca API. Defaults to "monaca-lib x.y.z". * @param {string} [options.language] - Can be either "en" or "ja". Defaults to "en". * @return {Promise} * @example * monaca.login('my@email.com', 'password').then( * function() { * // Login successful! * }, * function(error) { * // Login failed! * } * ); */ Monaca.prototype.login = function(email, password, options) { if (options) { return this._login(email, password, options); } else { return this._login(email, password); } }; /** * @method * @memberof Monaca * @description * Sign out from Monaca cloud. Will remove relogin token from disk and session tokens * from memory. * @return {Promise} * @example * monaca.login('my@email.com', 'password').then( * function() { * monaca.logout(); * } * ); */ Monaca.prototype.logout = function() { return this._get(this.apiRoot + '/user/logout') .finally( function() { this.setData({ 'reloginToken': '', 'clientId': '', 'x-monaca-param-api-token': '', 'x-monaca-param-session': '', 'trackId': '' }) .then( function() { this.tokens = { api: null, session: null }; this._loggedIn = false; }.bind(this) ); }.bind(this) ) }; /** * @method * @memberof Monaca * @description * Creates a new account in Monaca cloud using email and password. Returns a token used to check. * if the account has been activated. * @param {string} email - A Monaca account email. * @param {string} password - Password associated with the account. * @param {string} password_confirm - Password confirmation. * @return {Promise} * @example * monaca.signup('my@email.com', 'password', 'password').then( * function() { * // Signup successful! * }, * function(error) { * // Signup failed! * } * ); */ Monaca.prototype.signup = function(email, password, passwordConfirm, options) { options = options || {}; var deferred = Q.defer(); var form = { language: options.language || 'en', clientType: options.clientType || this.clientType || 'local', version: options.version || this.packageName + ' ' + this.version, os: os.platform(), register: { email: email, password: password, password_confirm: passwordConfirm } }; return this._post(this.webApiRoot + '/register', form) .then( function(data) { var body = this._safeParse(data.body); if (body.status === 'ok') { return Q.resolve(body.result.submitOK.token); } var errorMessage = body.title; Object.keys(body.result.formError).forEach(function(key) { errorMessage += '\n' + key + ': ' + body.result.formError[key]; }); return Q.reject(new Error(errorMessage)); }.bind(this), Q.reject ); }; /** * @method * @memberof Monaca * @description * Checks if the account related to the specified token is already activated. * @param {string} token - token related to a user account. * @return {Promise} * @example * monaca.isActivatedUser('token').then( * function() { * // Account is activated! * }, * function(error) { * // Account is not activated yet! * } * ); */ Monaca.prototype.isActivatedUser = function(token, options) { options = options || {}; var deferred = Q.defer(); return this._post(this.webApiRoot + '/check_activate', { language: options.language || 'en', clientType: options.clientType || this.clientType || 'local', version: options.version || this.packageName + ' ' + this.version, os: os.platform(), param: token }) .then( function(data) { var body = this._safeParse(data.body); if (body.status === 'ok') { return body.result === 1 ? Q.resolve() : Q.reject(); } return Q.reject(body.title); }.bind(this), Q.reject ); }; /** * Checks if the project can be built. * * @memberof Monaca * * @param {Object} projectInfo Contains project information such as projectId * @param {Object} buildParams Contains build parameters. Example: {platform: 'android', purpose: 'release', checkForMinimumRequirements: false} * * @example * monaca.checkBuildAvailability({ * projectId: 'myProjectID' * }, { * platform: 'android', * purpose: 'debug' * }).then( * () => { //Build the project }, * (err) => { //Cannot build the project } * ); * * @return {Promise} */ Monaca.prototype.checkBuildAvailability = function(projectInfo, buildParams) { if (!projectInfo || !projectInfo.projectId) { return Q.reject(new Error("Missing project info data.")); } else if(!buildParams || !buildParams.platform || !buildParams.purpose) { return Q.reject(new Error("Missing build parameters.")); } var projectId = projectInfo.projectId, platform = buildParams.platform, buildType = buildParams.purpose; let checkForMinimumRequirements = !!buildParams.checkForMinimumRequirements; return this._get('/project/' + projectId + '/can_build_app') .then( function(data) { var body = this._safeParse(data.body); if (body.status === 'ok') { var checkError = function() { var platformContent = body.result[platform]; if (!platformContent) { return 'Specified platform is not supported or doesn\'t exist.'; } if (!platformContent.has_remaining_slot) { return 'Your plan does not allow further builds at the moment. Please upgrade your account to start build, or try again later.'; } if (!platformContent.is_start_file_exist) { return 'Your project is missing the startup file (usually index.html).'; } // electron platform if (platform.startsWith('electron')) { if (platformContent.has_valid_cordova_version === false) return 'This cordova version is not supported.'; if (platformContent.has_valid_icon === false) return 'The icon is invalid. Icon should be at least 512x512 pixels.' } if (typeof platformContent.can_build_for[buildType] === 'undefined') { return platform + ' ' + buildType + ' build is not supported or doesn\'t exist.'; } // android platform if (platform === 'android') { if (!platformContent.is_versionname_valid) { return 'Version name is invalid.'; } if (buildType === 'release' && (!checkForMinimumRequirements && !platformContent.has_keysetting || !checkForMinimumRequirements && !platformContent.has_keystore)) { return 'Missing KeyStore configuration. Configure remote build by executing `monaca remote build --browser`.'; } } // ios platform if (platform === 'ios') { if (!platformContent.has_splash_and_icons) { return 'Your project is missing splash screens and/or icons. Please add the missing files from remote build settings by executing `monaca remote build --browser`.'; } if (buildType === 'debug') { if (!platformContent.has_dev_provisioning) { return 'Missing dev provisioning file. Please upload it from remote build settings by executing `monaca remote build --browser`.'; } if (platformContent.dev_provisioning_error) { return 'Error in dev provisioning file. Please upload again from remote build settings by executing `monaca remote build --browser`.'; } } else if (buildType === 'debugger') { if (!platformContent.has_debug_provisioning) { return 'Missing debug provisioning file. Please upload it from remote build settings by executing `monaca remote build --browser`.'; } if (platformContent.debug_provisioning_error) { return 'Error in debug provisioning file. Please upload again from remote build settings by executing `monaca remote build --browser`.'; } } else if (buildType === 'simulator') { return ''; // no need to check provisioning file. } else { if (!platformContent['has_' + buildType + '_provisioning']) { return 'Missing ' + buildType + ' provisioning file. Please upload it from remote build settings by executing `monaca remote build --browser`.'; } if (platformContent[buildType + '_provisioning_error']) { return 'Error in' + buildType + ' provisioning file. Please upload again from remote build settings by executing `monaca remote build --browser`.'; } } } return ''; }; var errorMessage = checkError(); if (errorMessage) { return Q.reject(new Error(errorMessage)); } return Q.resolve(body); } else { return Q.reject(new Error(body.status + " - " + body.message)); } }.bind(this), function(err) { var errMessage = err.message ? err.message : ''; if (err.code === 404) { return Q.reject(new Error("Error 404. Cannot reach the server, contact Monaca Support.")); } else { return Q.reject(new Error("Internal server error, contact Monaca Support. " + errMessage)); } } ); }; /** * @method * @memberof Monaca * @description * Generate a one time token for an URL. * @param {string} url * @return {Promise} */ Monaca.prototype.getSessionUrl = function(url) { var deferred = Q.defer(); this._get('/user/getSessionUrl', { url: url }).then( function(data) { deferred.resolve(this._safeParse(data.body).result.url); }.bind(this), function(error) { deferred.reject(error); } ); return deferred.promise; }; /** * @method * @memberof Monaca * @description * Downloads a file from Monaca request. * * If the download is successful the promise will resolve with the * downloaded filename. * @param {string} url - URL to download from * @param {object} data - Request parameters * @param {string} filename - Filename the data will be saved to. Can be a callback (promise) function. * @return {Promise} */ Monaca.prototype.download = function(url, data, filename) { var deferred = Q.defer(); var getFilename = (filename, response) => { var d = Q.defer(); if (typeof filename === 'string') { d.resolve(filename); } else if (typeof filename === 'function') { // Callback so that the caller can decide the filename from the response const dest = filename(response); if (typeof dest === 'string') { // if the function return string, return it right away d.resolve(dest); } else if (dest != null && dest.then != null && typeof dest.then === 'function') { // if the function return promise dest.then(data => { if (!data) { d.reject(new Error('Could not get data from dialog')); } else if (data.canceled) { d.reject(new Error('User cancel the save dialog')); } else if (!data.filePath) { d.reject(new Error('Not a valid file name.')); } else if (data.filePath) { d.resolve(data.filePath); } }); } } else { d.reject(new Error('Not a valid file name.')); } return d.promise; }; this._createRequestClient(data).then(function(requestClient) { requestClient.get(url) .on('response', function(response) { getFilename(filename, response).then( function (dest) { if (typeof dest === 'string') { var file = fs.createWriteStream(dest); response.pipe(file); file.on('finish', function() { deferred.resolve(dest); }); file.on('error', function(error) { deferred.reject(error); }); } else { deferred.reject(new Error('Not a valid file name.')); } }, function (e) { deferred.reject(e); } ); }) }.bind(this), function(error) { return deferred.reject(error); }); return deferred.promise; } /** * @method * @memberof Monaca * @name getLatestNews * @description * Fetches latest news and status on known issues from Monaca Cloud. * @param {Object} [options] Parameters * @param {Boolean} [options.disableStatusUpdate] * @return {Promise} */ Monaca.prototype.getLatestNews = function(options) { var deferred = Q.defer(); options = options || {}; options.disableStatusUpdate = options.disableStatusUpdate ? 1 : 0; this._get('/user/info/news', options ? options : {} ).then( function(data) { deferred.resolve(this._safeParse(data.body)); }.bind(this), function(error) { deferred.reject(error); } ); return deferred.promise; }; /** * @method * @memberof Monaca * @description * Fetch a list of all available projects. * @return {Promise} * @example * monaca.getProjects().then( * function(projects) { * console.log('You have ' + projects.length + ' projects!'); * }, * function(error) { * // Unable to fetch list. * } * ); */ Monaca.prototype.getProjects = function() { var deferred = Q.defer(); this._get('/user/projects').then( function(data) { deferred.resolve(this._safeParse(data.body).result.items); }.bind(this), function(error) { deferred.reject(error); } ); return deferred.promise; }; /** * @method * @memberof Monaca * @description * Get project ID. * @return {Promise} */ Monaca.prototype.getProjectId = function(projectDir) { return localProperties.get(projectDir, 'project_id'); }; /** * @method * @memberof Monaca * @description * Delete project ID. * @return {Promise} */ Monaca.prototype.deleteProjectId = function(projectDir) { return localProperties.del(projectDir, 'project_id'); }; /** * @method * @memberof Monaca * @description * Get local project ID. * @return {Promise} */ Monaca.prototype.getLocalProjectId = function(projectDir) { var deferred = Q.defer(), absolutePath = path.resolve(projectDir); try { var projectId = crypto.createHash('sha256').update(absolutePath).digest('hex'); deferred.resolve(projectId); } catch (error) { deferred.reject(error); } return deferred.promise; }; /** * @method * @memberof Monaca * @description * Set project ID. * @return {Promise} */ Monaca.prototype.setProjectId = function(projectDir, projectId) { return localProperties.set(projectDir, 'project_id', projectId); }; /** * @method * @memberof Monaca * @description * Fetch a list of files and directories for a project. * Must be logged in to use. * @param {string} projectId * @param {Array} ignoreList * @return {Promise} * @example * monaca.getRemoteProjectFiles('SOME_PROJECT_ID', ['node_modules', '.git']).then( * function(files) { * // Fetched file list! * }, * function(error) { * // Failed fetching file list! * } * ); */ Monaca.prototype.getRemoteProjectFiles = function(projectId, ignoreList) { var deferred = Q.defer(); utils.info('Reading Remote Files...', deferred, spinner); let readDirRecursive = (path) => { let readDirQueue = [], fileList = {}; let readDir = (path) => { let getItemList = (path) => { return new Promise((resolve,reject) => { utils.spinnerLoading(spinner, 'Reading Remote File: ' + path); this.getRemoteProjectFilesByPath(projectId, path) .then(files => { return resolve(files); }) .catch(err => { return reject(err); }); }); } let processItemList = (files) => { if (files && !utils.isEmptyObject(files)) { files = utils.filterIgnoreFiles(files, ignoreList, true); for (let fileKey in files) { let file = files[fileKey]; fileList[fileKey] = file; if (file.type === 'dir') { readDirQueue.push(fileKey); continue; } } } if (readDirQueue.length > 0) { return readDir(readDirQueue.shift()); } return fileList; } return getItemList(path) .then(processItemList); } return readDir(path); } readDirRecursive('/') .then(itemList => { utils.info('Reading Remote Files [FINISHED]', deferred, spinner); deferred.resolve(itemList); }) .catch(error => { utils.info('Reading Remote Files [ERROR]', deferred, spinner); if (error && (error.code === 404 || error.message === 'Not found')) utils.info(`\nThis project (${projectId}) is not existed in cloud.`, deferred); if (error && (error === 500 || error.code === 500)) utils.info(`\nIt seems that there is problem with this project (${projectId}).\nPlease verify 'project_id' in '.monaca/local_properties.json' and this project in the cloud.`, deferred); deferred.reject(error); }); return deferred.promise; }; /** * @method * @memberof Monaca * @description * Fetch a list of files and directories for a project. * Must be logged in to use. * @param {string} projectId * @param {string} path * @return {Promise} * @example * monaca.getRemoteProjectFilesByPath('SOME_PROJECT_ID', '/').then( * function(files) { * // Fetched file list! * }, * function(error) { * // Failed fetching file list! * } * ); */ Monaca.prototype.getRemoteProjectFilesByPath = function(projectId, path = '/') { return new Promise((resolve, reject) => { this._post('/project/' + projectId + '/file/tree/byPath/' + encodeURIComponent(path), { path: path }) .then( data => { let files = null;; if (data && data.body) files = JSON.parse(data.body).result.items; resolve(files); }) .catch( err => { reject(err); }); }); } /** * @method * @memberof Monaca * @description * Fetch a list of files and directories for a project. * Must be logged in to use. * @param {string} projectId * @return {Promise} * @example * monaca.getProjectFiles('SOME_PROJECT_ID').then( * function(files) { * // Fetched file list! * }, * function(error) { * // Failed fetching file list! * } * ); */ Monaca.prototype.getProjectFiles = function(projectId) { var deferred = Q.defer(); utils.info('Reading Remote Files...', deferred); this._post('/project/' + projectId + '/file/tree', {ignoredNodeModules: true}).then( function(data) { utils.info('Reading Remote Files [FINISHED]', deferred); deferred.resolve(JSON.parse(data.body).result.items); }, function(error) { utils.info('Reading Remote Files [ERROR]', deferred); if (error && (error.code === 404 || error.message === 'Not found')) utils.info(`\nThis project (${projectId}) is not existed in cloud.`, deferred); if (error && (error === 500 || error.code === 500)) utils.info(`\nIt seems that there is problem with this project (${projectId}).\nPlease verify 'project_id' in '.monaca/local_properties.json' and this project in the cloud.`, deferred); deferred.reject(error); } ); return deferred.promise; }; /** * @method * @memberof Monaca * @description * Fetch a list of files and directories for a local project. * @param {string} projectDir - Path to project. * @param {Object} [options] Parameters like filter to filter from list of files. * @return {Promise} * @example * monaca.getLocalProjectFiles = function('/some/directory').then( * function(files) { * // Successfully fetched file list! * }, * function(error) { * // Failed fetching file list! * } * ); */ Monaca.prototype.getLocalProjectFiles = function(projectDir, options) { let deferred = Q.defer(); let qLimit = qlimit(100); let getFileChecksum = qLimit(function(file, isSymbolicLink) { let deferred = Q.defer(); utils.spinnerLoading(spinner, 'Reading Local File: ' + file); if (isSymbolicLink) { fs.readlink(file, function(error, data) { if (error) { deferred.reject(error); } else { deferred.resolve(crc32(data).toString('hex')); } }); } else { fs.readFile(file, function(error, data) { if (error) { deferred.reject(error); } else { deferred.resolve(crc32(data).toString('hex')); } }); } return deferred.promise; }); utils.info('Reading Local Files...', deferred, spinner); fs.exists(projectDir, function(exists) { if (exists) { let files = {}, promises = [], filteredList = []; try { // create .monacaignore if there isn't let ignoreList = this._filterMonacaIgnore(projectDir); // Read all files from project directory filteredList = glob.sync("**/*", { cwd: path.resolve(projectDir), dot: true, } ); // filter list from .monacaignore filteredList = utils.filter(filteredList, ignoreList); // user-defined filter functions if (options && options.filter && typeof options.filter === 'function') { filteredList = filteredList.filter(options.filter); } } catch(error) { return deferred.reject(error); } utils.info('Reading Local Files [Calculating File Checksum]', deferred, spinner); filteredList.forEach(function(file) { let obj = {}, key = path.join('/', file); // Converting Windows path delimit