UNPKG

ionic

Version:

A tool for creating and developing Ionic Framework mobile apps.

892 lines (727 loc) • 26.7 kB
var fs = require('fs'), Q = require('q'), path = require('path'), argv = require('optimist').argv, connect = require('connect'), finalhandler = require('finalhandler'), http = require('http'), serveStatic = require('serve-static'), tinylr = require('tiny-lr-fork'), lr = require('connect-livereload'), vfs = require('vinyl-fs'), request = require('request'), IonicProject = require('./project'), Task = require('./task').Task, proxyMiddleware = require('proxy-middleware'), url = require('url'), xml2js = require('xml2js'), IonicStats = require('./stats').IonicStats, shelljs = require('shelljs'), ports = require('./ports'); var DEFAULT_HTTP_PORT = 8100; var DEFAULT_LIVE_RELOAD_PORT = 35729; var IONIC_LAB_URL = '/ionic-lab'; var IonicTask = function() {}; IonicTask.prototype = new Task(); IonicTask.prototype.run = function(ionic) { var self = this; this.ionic = ionic; this.port = argv._[1]; this.liveReloadPort = argv._[2]; this.loadSettings(); this.getAddress() .then(function() { return self.checkPorts(true, self.port, self.address); }) .then(function() { if(self.runLivereload) { return self.checkPorts(false, self.liveReloadPort, self.address); } }) .then(function() { if(self.isAddressCmd) { console.log( self.address ); process.exit(); } self.start(ionic) // self.printCommandTips(); // console.log('listenForServerCommands') // self.listenForServerCommands(); if(ionic.hasFailed) return; ionic.latestVersion.promise.then(function(){ ionic.printVersionWarning(); }); }) .catch(function(error) { }) }; IonicTask.prototype.listenForServerCommands = function listenForServerCommands() { var self = this; var readline = require('readline'); process.on("SIGINT", function(){ process.exit(); }); var rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); if(process.platform === "win32") { rl.on("SIGINT", function (){ process.emit("SIGINT"); }); } rl.setPrompt('ionic $ '); rl.prompt(); rl.on('line', function(entry) { if(entry === null) return; // console.log('entry', entry) var input = (entry + '').trim(); if (input === 'quit' || input === 'q') rl.close(); if (input === null) return; input = (input + '').trim(); // console.log('input received:', input) if(input == 'restart' || input == 'r') { self._goToUrl('/?restart=' + Math.floor((Math.random() * 899999) + 100000)); } else if(input.indexOf('goto ') === 0 || input.indexOf('g ') === 0) { var url = input.replace('goto ', '').replace('g ', ''); self._goToUrl(url); } else if(input == 'consolelogs' || input == 'c') { self.printConsoleLogs = !self.printConsoleLogs; console.log('Console log output: '.green + (self.printConsoleLogs ? 'enabled' : 'disabled')); self._goToUrl('/?restart=' + Math.floor((Math.random() * 899999) + 100000)); } else if(input == 'serverlogs' || input == 's') { self.printServerLogs = !self.printServerLogs; console.log('Server log output: '.green + (self.printServerLogs ? 'enabled' : 'disabled')); } else if(input.match(/^go\([+\-]?[0-9]{1,9}\)$/)) { self._goToHistory(input); } else if(input == 'help' || input == 'h') { self.printCommandTips(); } else if(input == 'quit' || input == 'q') { if(self.childProcess) { self.childProcess.kill('SIGTERM'); } process.exit(); } else if(input == 'clear' || input == 'clr') { process.stdout.write("\u001b[2J\u001b[0;0H"); } else { console.log('\nInvalid ionic server command'.error.bold); self.printCommandTips(); } // console.log('input: ', input); rl.prompt(); }).on('close', function() { process.exit(0); }); } //isIonicServer = true when serving www directory, false when live reload IonicTask.prototype.checkPorts = function(isIonicServer, testPort, testHost) { var q = Q.defer(); var self = this, message = []; if(isIonicServer) { testHost = this.address == 'localhost' ? null : this.address; } else { testHost = null; } ports.getPort({port: testPort, host: testHost}, function(err, port) { if(port != testPort) { message = ['The port ', testPort, ' was taken on the host ', self.address, ' - using port ', port, ' instead'].join(''); console.log(message.yellow.bold); if(isIonicServer) { self.port = port; } else { self.liveReloadPort = port; } } q.resolve(); }); return q.promise; } IonicTask.prototype.loadSettings = function(cb) { var project = null; try { project = IonicProject.load(); } catch (ex) { this.ionic.fail(ex.message); return } var self = this; this.port = this.port || argv.port || argv.p || DEFAULT_HTTP_PORT; this.liveReloadPort = this.liveReloadPort || argv.livereloadport || argv.r || argv['livereload-port'] || argv.i || DEFAULT_LIVE_RELOAD_PORT; this.launchBrowser = !argv.nobrowser && !argv.b; this.launchLab = this.launchBrowser && (argv.lab || argv.l); this.runLivereload = !(argv.nolivereload || argv.d); this.useProxy = !argv.noproxy && !argv.x; this.proxies = project.get('proxies') || []; this.watchSass = project.get('sass') === true && !argv.nosass && !argv.n; this.gulpStartupTasks = project.get('gulpStartupTasks'); this.browser = argv.browser || argv.w || ''; this.browserOption = argv.browserOption || argv.o || ''; //Check for default browser being specified this.defaultBrowser = argv.defaultBrowser || argv.f || project.get('defaultBrowser'); if(this.defaultBrowser) { project.set('defaultBrowser', this.defaultBrowser); project.save(); } this.browser = this.browser || this.defaultBrowser; this.watchPatterns = project.get('watchPatterns') || ['www/**/*', '!www/lib/**/*']; this.printConsoleLogs = argv.consolelogs || argv['console-logs'] || argv.c; this.printServerLogs = argv.serverlogs || argv['server-logs'] || argv.s; this.isAddressCmd = argv._[0].toLowerCase() == 'address'; this.documentRoot = project.get('documentRoot') || 'www'; this.createDocumentRoot = project.get('createDocumentRoot') || null; this.contentSrc = path.join(this.documentRoot, this.ionic.getContentSrc()); }; IonicTask.prototype.printCommandTips = function(ionic) { console.log('Ionic server commands, enter:'.green.bold); console.log(' restart' + ' or '.green + 'r' + ' to restart the client app from the root'.green); console.log(' goto' + ' or '.green + 'g' + ' and a url to have the app navigate to the given url'.green); console.log(' consolelogs' + ' or '.green + 'c' + ' to enable/disable console log output'.green); console.log(' serverlogs' + ' or '.green + 's' + ' to enable/disable server log output'.green); console.log(' quit' + ' or '.green + 'q' + ' to shutdown the server and exit'.green); console.log(''); }; IonicTask.prototype.gulpInstalledGlobally = function gulpInstalledGlobally() { var result = shelljs.exec('gulp -v', { silent: true }); if(result.code != 0) { return false; } return true; } IonicTask.prototype.showFinishedServeMessage = function() { if(this.launchLab || this.launchBrowser) { var open = require('open'); var openUrl = this.launchLab ? [this.host(this.port), IONIC_LAB_URL] : [this.host(this.port)]; if(this.browserOption) { openUrl.push(this.browserOption) } try { open(openUrl.join(''), this.browser); } catch (ex) { console.log('Error opening the browser - ', ex) } } this.printCommandTips(); this.listenForServerCommands(); } IonicTask.prototype.start = function(ionic) { try { var self = this; var app = connect(); var childProcess = null; if (!fs.existsSync( path.resolve(this.documentRoot) )) { if (this.createDocumentRoot) { fs.mkdirSync(this.documentRoot); } else { return ionic.fail('"' + this.documentRoot + '" directory cannot be found. Please make sure the working directory is an Ionic project.'); } } // gulpStartupTasks should be an array of tasks set in the project config // watchSass is for backwards compatible sass: true project config if((this.gulpStartupTasks && this.gulpStartupTasks.length) || this.watchSass) { var tasks = this.gulpStartupTasks || ['sass','watch']; if(!self.gulpInstalledGlobally()) { var message = ['You have specified Gulp start up tasks in your ionic.project file.'.red, '\n', 'However, you do not have Gulp installed globally. Please run '.red, '`npm install -g gulp`'.green].join(''); return ionic.fail(message) } console.log('Gulp startup tasks:'.green.bold, tasks); self.childProcess = require('cross-spawn').spawn('gulp', tasks, { stdio: 'inherit' }); } if(this.runLivereload) { vfs.watch(self.watchPatterns, {},function(f) { self._changed(f.path); }); self.liveReloadServer = self.host(self.liveReloadPort); var lrServer = tinylr(); try { lrServer.listen(self.liveReloadPort, function(err) { if(err) { return ionic.fail('Unable to start live reload server:', err); } else { console.log('Running live reload server:'.green.bold, self.liveReloadServer ); console.log('Watching :'.green.bold, self.watchPatterns); self.showFinishedServeMessage(); } }); } catch(ex) { self.ionic.fail('Live Reload failed to start, error: ', ex.message) } app.use(require('connect-livereload')({ port: this.liveReloadPort })); } if(this.useProxy) { for(var x=0; x<this.proxies.length; x++) { var proxy = this.proxies[x]; var opts = url.parse(proxy.proxyUrl); if(proxy.proxyNoAgent) opts.agent = false; app.use(proxy.path, proxyMiddleware(opts)); console.log('Proxy added:'.green.bold, proxy.path + " => " + url.format(proxy.proxyUrl)); } } this.devServer = this.host(this.port); // Serve up the www folder by default var serve = serveStatic(this.documentRoot); // Create static server var server = http.createServer(function(req, res){ var done = finalhandler(req, res); var urlParsed = url.parse(req.url, true); var platformOverride = urlParsed.query && urlParsed.query.ionicplatform; var platformUrl = getPlatformUrl(req); if(platformUrl) { var platformWWW = getPlatformWWW(req); if(self.isPlatformServe) { fs.readFile( path.resolve(path.join(platformWWW, platformUrl)), function (err, buf) { res.setHeader('Content-Type', 'application/javascript'); if (err) { res.end('// mocked cordova.js response to prevent 404 errors during development'); if(req.url == '/cordova.js') { self.serverLog(req, '(mocked)'); } else { self.serverLog(req, '(Error ' + platformWWW + ')'); } } else { self.serverLog(req, '(' + platformWWW + ')'); res.end(buf); } }); } else { self.serverLog(req, '(mocked)'); res.setHeader('Content-Type', 'application/javascript'); res.end('// mocked cordova.js response to prevent 404 errors during development'); } return; } if(self.printConsoleLogs && req.url === '/__ionic-cli/console') { self.consoleLog(req); res.end(''); return; } if(req.url === IONIC_LAB_URL) { // Serve the lab page with the given object with template data var labServeFn = function(context) { fs.readFile(path.resolve(path.join(__dirname, 'assets/preview.html')), function(err, buf) { var html; if(err) { res.end('404'); } else { html = buf.toString('utf8'); html = html.replace('//INSERT_JSON_HERE', 'var BOOTSTRAP = ' + JSON.stringify(context || {})); } res.setHeader('Content-Type', 'text/html'); res.end(html); }); }; // If the config.xml file exists, let's parse it for some nice features like // showing the name of the app in the title if(fs.existsSync('config.xml')) { fs.readFile(path.resolve('config.xml'), function(err, buf) { var xml = buf.toString('utf8'); xml2js.parseString(xml, function (err, result) { labServeFn({ appName: result.widget.name[0] }); }); }); } else { labServeFn(); } return; } if(req.url.split('?')[0] === '/') { fs.readFile( path.resolve(self.contentSrc), 'utf8', function (err, buf) { res.setHeader('Content-Type', 'text/html'); if (err) { self.serverLog(req, 'ERROR!'); res.end(err.toString()); } else { self.serverLog(req, '(' + self.contentSrc + ')'); var html = injectGoToScript( buf.toString('utf8') ); if(self.printConsoleLogs) { html = injectConsoleLogScript(html); } if(platformOverride) { html = injectPlatformScript( html, platformOverride ); } res.end(html); } }); return; } // root www directory file self.serverLog(req); serve(req, res, done); }); // Listen app.use(server); try { app.listen(this.port, this.address); }catch(ex) { self.ionic.fail('Failed to start the Ionic server: ', ex.message) } console.log('Running dev server:'.green.bold, this.devServer); if(!self.runLivereload) { self.showFinishedServeMessage(); } } catch(e) { var msg; if(e && (e + '').indexOf('EMFILE') > -1) { msg = (e + '\n').error.bold + 'The watch process has exceed the default number of files to keep open.\n'.error.bold + 'You can change the default with the following command:\n\n'.error.bold + ' ulimit -n 1000\n\n' + 'In the command above, it\'s setting the default to watch a max of 1000 files.\n\n'.error.bold; } else { msg = ('server start error: ' + e.stack).error.bold; } console.log(msg); process.exit(1); } }; IonicTask.prototype.serverLog = function(req, msg) { if(this.printServerLogs) { var log = 'serve '.yellow; log += (req.url.length > 60 ? req.url.substr(0, 57) + '...' : req.url).yellow; if(msg) { log += ' ' + msg.yellow; } var ua = (req.headers && req.headers['user-agent'] || ''); if(ua.indexOf('Android') > 0) { log += ' Android'.small; } else if(ua.indexOf('iPhone') > -1 || ua.indexOf('iPad') > -1 || ua.indexOf('iPod') > -1) { log += ' iOS'.small; } else if(ua.indexOf('Windows Phone') > -1) { log += ' Windows Phone'.small; } console.log(log); } }; IonicTask.prototype.consoleLog = function(req) { var body = ''; req.on('data', function (data) { if(data) body += data; }); req.on('end', function () { if(!body) return; try { var log = JSON.parse(body); var msg = log.index + ' '; while(msg.length < 5) { msg += ' '; } msg += ' ' + (log.ts + '').substr(7) + ' '; msg += log.method; while(msg.length < 24) { msg += ' '; } var msgIndent = ''; while(msgIndent.length < msg.length) { msgIndent += ' '; } if(log.method == 'dir' || log.method == 'table') { var isFirstLine = true; log.args.forEach(function(argObj){ for(objKey in argObj) { if(isFirstLine) { isFirstLine = false; } else { msg += '\n' + msgIndent; } msg += objKey + ': '; try { msg += ( JSON.stringify(argObj[objKey], null, 1) ).replace(/\n/g, ''); } catch(e) { msg += argObj[objKey]; } } }); } else if(log.args.length) { if(log.args.length === 2 && log.args[0] === '%o' && log.args[1] == '[object Object]') return; msg += log.args.join(', '); } if(log.method == 'error' || log.method == 'exception') msg = msg.red; else if(log.method == 'warn') msg = msg.yellow; else if(log.method == 'info') msg = msg.green; else if(log.method == 'debug') msg = msg.blue; console.log(msg); }catch(e){} }); }; function getPlatformUrl(req) { if(req.url == '/cordova.js' || req.url == '/cordova_plugins.js' || req.url.indexOf('/plugins/') === 0) { return req.url; } } function getPlatformWWW(req) { var platformPath = 'www'; if(req && req.headers && req.headers['user-agent']) { var ua = req.headers['user-agent'].toLowerCase(); if(ua.indexOf('iphone') > -1 || ua.indexOf('ipad') > -1 || ua.indexOf('ipod') > -1) { platformPath = path.join('platforms', 'ios', 'www'); } else if(ua.indexOf('android') > -1) { platformPath = path.join('platforms', 'android', 'assets', 'www'); } } return platformPath; } function injectConsoleLogScript(html) { try{ var findTags = html.match(/<html(?=[\s>])(.*?)>|<head>|<meta charset(.*?)>/gi); var insertAfter = findTags[ findTags.length - 1 ]; return html.replace(insertAfter, insertAfter + '\n\ <script>\n\ // Injected Ionic CLI Console Logger\n\ (function() {\n\ var methods = "assert clear count debug dir dirxml error exception group groupCollapsed groupEnd info log markTimeline profile profileEnd table time timeEnd timeStamp trace warn".split(" ");\n\ var console = (window.console=window.console || {});\n\ var logCount = 0;\n\ window.onerror = function(msg, url, line) {\n\ if(msg && url) console.error(msg, url, (line ? "Line: " + line : ""));\n\ };\n\ function sendConsoleLog(method, args) {\n\ try {\n\ var xhr = new XMLHttpRequest();\n\ xhr.open("POST", "/__ionic-cli/console", true);\n\ xhr.send(JSON.stringify({ index: logCount, method: method, ts: Date.now(), args: args }));\n\ logCount++;\n\ } catch(e){}\n\ }\n\ for(var x=0; x<methods.length; x++) {\n\ (function(m){\n\ var orgConsole = console[m];\n\ console[m] = function() {\n\ try {\n\ sendConsoleLog(m, Array.prototype.slice.call(arguments));\n\ if(orgConsole) orgConsole.apply(console, arguments);\n\ } catch(e){}\n\ };\n\ })(methods[x]);\n\ }\n\ }());\n\ </script>'); }catch(e){} return html; } function injectGoToScript(html) { try{ var findTags = html.match(/<html(?=[\s>])(.*?)>|<head>|<meta charset(.*?)>/gi); var insertAfter = findTags[ findTags.length - 1 ]; return html.replace(insertAfter, insertAfter + '\n\ <script>\n\ // Injected Ionic Go To URL Live Reload Plugin\n\ window.LiveReloadPlugin_IonicGoToUrl = (function() {\n\ var GOTO_KEY = "__ionic_goto_url__";\n\ var HISTORY_GO_KEY = "__ionic_history_go__";\n\ var GoToUrlPlugin = function(window, host) {\n\ this.window = window;\n\ this.host = host;\n\ }\n\ GoToUrlPlugin.identifier = "__ionic_goto_url__";\n\ GoToUrlPlugin.version = "1.0";\n\ GoToUrlPlugin.prototype.reload = function(path) {\n\ try {\n\ if(path) {\n\ if(path.indexOf(GOTO_KEY) === 0) {\n\ this.window.document.location = path.replace(GOTO_KEY, "");\n\ return true;\n\ }\n\ if(path.indexOf(HISTORY_GO_KEY) === 0) {\n\ this.window.document.history.go( parseInt(path.replace(HISTORY_GO_KEY, ""), 10));\n\ return true;\n\ }\n\ }\n\ } catch(e) {\n\ console.log(e);\n\ }\n\ return false;\n\ };\n\ return GoToUrlPlugin;\n\ })();\n\ </script>'); }catch(e){} return html; } /** * Inject the platform override for choosing Android or iOS during * development. */ function injectPlatformScript(html, platformOverride) { try { var findTags = html.toLowerCase().indexOf('</body>'); if(findTags < 0) { return html; } return html.slice(0, findTags) + '\n' + '<script>\n' + 'ionic && ionic.Platform && ionic.Platform.setPlatform("' + platformOverride + '");\n' + '</script>\n' + html.slice(findTags); } catch(e) {} return html; } IonicTask.prototype._changed = function(filePath) { // Cleanup the path a bit var pwd = process.cwd(); filePath = filePath.replace(pwd + '/', ''); if( filePath.indexOf('.css') > 0 ) { console.log( ('CSS changed: ' + filePath).green ); } else if( filePath.indexOf('.js') > 0 ) { console.log( ('JS changed: ' + filePath).green ); } else if( filePath.indexOf('.html') > 0 ) { console.log( ('HTML changed: ' + filePath).green ); } else { console.log( ('File changed: ' + filePath).green ); } this._postToLiveReload( [filePath] ); }; IonicTask.prototype._goToUrl = function(url) { console.log( ('Loading: ' + url).green ); this._postToLiveReload( ['__ionic_goto_url__' + url] ); }; IonicTask.prototype._goToHistory = function(goHistory) { goHistory = goHistory.replace('go(', '').replace(')', ''); console.log( ('History Go: ' + goHistory).green ); this._postToLiveReload( ['__ionic_history_go__' + goHistory] ); }; IonicTask.prototype._postToLiveReload = function(files) { var postUrl = [this.liveReloadServer, '/changed'].join('') request.post(postUrl, { path: '/changed', method: 'POST', body: JSON.stringify({ files: files }) }, function(err, res, body) { if(err) { console.log('Unable to update live reload:', err); } }); } IonicTask.prototype.getAddress = function(cb) { var q = Q.defer(); try { var self = this; var addresses = []; var os = require('os'); var ifaces = os.networkInterfaces(); var ionicConfig = require('./config').load(); var addressConfigKey = (self.isPlatformServe ? 'platformServeAddress' : 'ionicServeAddress'); var tryAddress; if(self.isAddressCmd) { // reset any address configs ionicConfig.set('ionicServeAddress', null); ionicConfig.set('platformServeAddress', null); } else { if(!argv.address) tryAddress = ionicConfig.get(addressConfigKey); else tryAddress = argv.address; } if(ifaces){ for (var dev in ifaces) { if(!dev) continue; ifaces[dev].forEach(function(details){ if (details && details.family == 'IPv4' && !details.internal && details.address) { addresses.push({ address: details.address, dev: dev }); } }); } } if(tryAddress) { if(tryAddress == 'localhost') { self.address = tryAddress; // cb(); q.resolve(); return q.promise; } for(var x=0; x<addresses.length; x++) { // double check if this address is still available if(addresses[x].address == tryAddress) { self.address = addresses[x].address; // cb(); q.resolve(); return q.promise; } } if (argv.address) { self.ionic.fail('Address ' + argv.address + ' not available.'); return q.promise; } } if(addresses.length > 0) { if(!self.isPlatformServe) { addresses.push({ address: 'localhost' }); } if(addresses.length === 1) { self.address = addresses[0].address; // cb(); q.resolve(); return q.promise; } console.log('\nMultiple addresses available.'.error.bold); console.log('Please select which address to use by entering its number from the list below:'.error.bold); if(self.isPlatformServe) { console.log('Note that the emulator/device must be able to access the given IP address'.small); } for(var x=0; x<addresses.length; x++) { console.log( (' ' + (x+1) + ') ' + addresses[x].address + ( addresses[x].dev ? ' (' + addresses[x].dev + ')' : '' )).yellow ); } console.log('Std in before prompt') // console.log(process.stdin) var prompt = require('prompt'); var promptProperties = { selection: { name: 'selection', description: 'Address Selection: '.yellow.bold, required: true } }; prompt.override = argv; prompt.message = ''; prompt.delimiter = ''; prompt.start(); prompt.get({properties: promptProperties}, function (err, promptResult) { if(err) { return console.log(err); } var selection = promptResult.selection; for(var x=0; x<addresses.length; x++) { if(selection == (x + 1) || selection == addresses[x].address || selection == addresses[x].dev) { self.address = addresses[x].address; if(!self.isAddressCmd) { console.log('Selected address: '.green.bold + self.address); } ionicConfig.set(addressConfigKey, self.address); // cb(); prompt.resume(); q.resolve(); return q.promise; } } self.ionic.fail('Invalid address selection'); }); } else if(self.isPlatformServe) { // no addresses found self.ionic.fail('Unable to find an IPv4 address for run/emulate live reload.\nIs WiFi disabled or LAN disconnected?'); } else { // no address found, but doesn't matter if it doesn't need an ip address and localhost will do self.address = 'localhost'; // cb(); q.resolve(); } } catch(e) { self.ionic.fail('Error getting IPv4 address: ' + e); } return q.promise; }; IonicTask.prototype.host = function(port) { return 'http://' + this.address + ':' + port; }; exports.IonicTask = IonicTask;