diff --git a/Gruntfile.js b/Gruntfile.js index 6504749..4bd13f2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -215,7 +215,8 @@ module.exports = function( grunt ) { apiPrefix: '/api', apiBaseUrl: 'configure-to-specific-api', proxyPort: '80',// change to 443 for https - proxyProtocol: 'http'//change to https if ssl is required + proxyProtocol: 'http', //change to https if ssl is required + livereload: true } }, prod: { @@ -223,7 +224,20 @@ module.exports = function( grunt ) { port: process.env.PORT || 8080, hostname: '0.0.0.0', base: 'build/www', - apiPrefix: '/api*' + apiPrefix: '/api*', + compress: true, + preRender: { + cache: { + size: 100 + } + }, + // Uncommenting will server all traffic through SPDY/HTTPS. +// ssl: { +// port: 443, +// key: 'keys/server.key', +// cert: 'keys/server.crt', +// ca: 'keys/server.csr' +// } } }, doc: { diff --git a/bower.json b/bower.json index 93ee774..3ff7471 100644 --- a/bower.json +++ b/bower.json @@ -3,7 +3,7 @@ "version": "2.3.3", "homepage": "http://getlavaca.com", "authors": [ - "Mutual Mobile " + "Mutual Mobile " ], "license": "MIT", "ignore": [ @@ -14,6 +14,7 @@ "tests" ], "dependencies": { - "lavaca": "latest" + "lavaca": "latest", + "less.js": "1.7.x" } } diff --git a/package.json b/package.json index 0d90679..b94dd80 100755 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "fs-extra": "~0.6.3", "grunt": "0.4.1", "grunt-cli": "~0.1.9", - "express": "3.1.x", + "express": "4.1.1", "underscore": "1.4.x", "preprocess": "1.0.x", "connect": "2.4.4", @@ -30,7 +30,15 @@ "dustjs-linkedin": "2.0.2", "dustjs-helpers": "1.1.1", "grunt-shell": "0.3.x", - "bower": "1.2.x" + "bower": "1.2.x", + "node-phantom": "0.2.5", + "lru-cache": "2.5.0", + "body-parser": "^1.0.2", + "compression": "^1.0.2", + "errorhandler": "^1.0.1", + "vhost": "^1.0.0", + "express-livereload": "0.0.24", + "spdy": "^1.25.6" }, "scripts": { "postinstall": "./node_modules/bower/bin/bower install" diff --git a/src/www/favicon.ico b/src/www/favicon.ico new file mode 100644 index 0000000..197ea1f Binary files /dev/null and b/src/www/favicon.ico differ diff --git a/src/www/index.html b/src/www/index.html index 2826da2..bb4fd6b 100755 --- a/src/www/index.html +++ b/src/www/index.html @@ -23,32 +23,31 @@ - + - - + + - - + + - + - + - + - + - @@ -61,22 +60,26 @@ var less=less||{}; less.env='development'; - + + - + - - + + - + + diff --git a/src/www/js/app/app.js b/src/www/js/app/app.js index 034f711..2e1c3a8 100755 --- a/src/www/js/app/app.js +++ b/src/www/js/app/app.js @@ -1,4 +1,5 @@ define(function(require) { + //require('less'); var History = require('lavaca/net/History'); var HomeController = require('./net/HomeController'); var Connectivity = require('lavaca/net/Connectivity'); @@ -11,7 +12,7 @@ define(function(require) { // Uncomment this section to use hash-based browser history instead of HTML5 history. // You should use hash-based history if there's no server-side component supporting your app's routes. - History.overrideStandardsMode(); + // History.overrideStandardsMode(); /** * Global application-specific object @@ -21,7 +22,8 @@ define(function(require) { var app = new Application(function() { // Add routes this.router.add({ - '/': [HomeController, 'index'] + '/': [HomeController, 'index'], + '/test': [HomeController, 'test'] }); // Initialize messages Translation.init('en_US'); diff --git a/src/www/js/app/boot.js b/src/www/js/app/boot.js index 080db56..0f59386 100644 --- a/src/www/js/app/boot.js +++ b/src/www/js/app/boot.js @@ -1,5 +1,5 @@ require.config({ - baseUrl: 'js', + baseUrl: '/js', paths: { 'es5-shim': '../components/es5-shim/es5-shim', '$': '../components/jquery/jquery', @@ -10,6 +10,7 @@ require.config({ 'dust-helpers': '../components/dustjs-linkedin-helpers/dist/dust-helpers-1.1.1', 'rdust': '../components/require-dust/require-dust', 'iScroll': '../components/iscroll/dist/iscroll-lite-min', + 'less': '../components/less.js/dist/less-1.7.0', 'lavaca': '../components/lavaca/src' }, shim: { diff --git a/src/www/js/app/net/HomeController.js b/src/www/js/app/net/HomeController.js index d28d39f..052e960 100755 --- a/src/www/js/app/net/HomeController.js +++ b/src/www/js/app/net/HomeController.js @@ -23,6 +23,13 @@ define(function(require) { return this .view(null, HomeView, model) .then(this.updateState(history, 'Home Page', params.url)); + }, + test: function(params, history) { + var model = new Model(); + model.set('test', 'test'); + return this + .view(null, HomeView, model) + .then(this.updateState(history, 'Home Page', params.url)); } }); diff --git a/src/www/js/app/ui/views/BaseView.js b/src/www/js/app/ui/views/BaseView.js index 508e786..694d9fe 100755 --- a/src/www/js/app/ui/views/BaseView.js +++ b/src/www/js/app/ui/views/BaseView.js @@ -18,6 +18,13 @@ define(function(require) { var BaseView = View.extend(function() { View.apply(this, arguments); this.mapEvent('.cancel', 'tap', this.onTapCancel); + if (typeof window.callPhantom === 'function') { + this.on('entercomplete', function() { + window.callPhantom({ + event: 'enterComplete' + }); + }); + } }, { /** diff --git a/src/www/js/templates/home.html b/src/www/js/templates/home.html index 5abce3b..dff9c19 100755 --- a/src/www/js/templates/home.html +++ b/src/www/js/templates/home.html @@ -1 +1,2 @@ -

{@msg key="headline"/}

\ No newline at end of file +{?test}

TEST

{/test} +

{@msg key="headline"/}

diff --git a/src/www/robots.txt b/src/www/robots.txt new file mode 100644 index 0000000..c3b8430 --- /dev/null +++ b/src/www/robots.txt @@ -0,0 +1 @@ +User-agent: * Disallow: Sitemap: /seo \ No newline at end of file diff --git a/tasks/server/server.js b/tasks/server/server.js index b01fdd7..aab614b 100755 --- a/tasks/server/server.js +++ b/tasks/server/server.js @@ -1,21 +1,30 @@ -module.exports = function(grunt) { +module.exports = function (grunt) { 'use strict'; /* server.js */ var express = require('express'), - util = require('util'); + util = require('util'), + phantom = require('node-phantom'), + LRU = require('lru-cache'), + bodyParser = require('body-parser'), + compression = require('compression'), + errorhandler = require('errorhandler'), + vhost = require('vhost'), + livereload = require('express-livereload'), + spdy = require('spdy'), + fs = require('fs'); - var startServer = function(config) { + var startServer = function (config) { config = config || {}; var requester = require(config.proxyProtocol); - var server = express(); - var hourMs = config.hourMs || 0*60*60, - vhost = config.vhost || 'localhost', - base = config.base, - port = config.port, - host = config.host, - apiPrefix = config.apiPrefix || '/api', - basicAuth = config.basicAuth; + var app = express(); + var hourMs = config.hourMs || 0 * 60 * 60, + domain = config.vhost || 'localhost', + base = config.base, + port = config.port, + host = config.host, + apiPrefix = config.apiPrefix || '/api', + basicAuth = config.basicAuth; function proxyRequest(request, response) { var postData = request.body; @@ -34,19 +43,19 @@ module.exports = function(grunt) { if ('POST' === request.method && typeof postData === 'object') { postData = JSON.stringify(postData); } - var req = requester.request(options, function(res) { + var req = requester.request(options, function (res) { var output = ''; - console.log(options.method + ' @ ' + options.host + options.path + ' Code: '+ res.statusCode); + console.log(options.method + ' @ ' + options.host + options.path + ' Code: ' + res.statusCode); res.setEncoding('utf8'); res.on('data', function (chunk) { output += chunk; }); - res.on('end', function() { + res.on('end', function () { response .status(res.statusCode); try { jsonData = JSON.parse(output); - } catch(e) { + } catch (e) { jsonData = null; } if (typeof jsonData === 'object') { @@ -57,7 +66,7 @@ module.exports = function(grunt) { }); }); - req.on('error', function(e) { + req.on('error', function (e) { console.log('problem with request: ' + e.message); }); @@ -68,45 +77,147 @@ module.exports = function(grunt) { req.end(); } - server.use(express['static'](base, {maxAge: hourMs})); - server.use(express.directory(base, {icons: true})); - server.use(express.bodyParser()); - server.use(express.errorHandler({dumpExceptions: true, showStack: true})); + config.compress && app.use(compression()); + app.all(apiPrefix + '*', proxyRequest); + app.use(bodyParser()); + app.use(errorhandler({dumpExceptions: true, showStack: true})); + + if (config.preRender) { + if (config.preRender.cache) { + var cache = LRU({ + max: config.preRender.cache.size // number of pages to cache + }); + } + + phantom.create(function (err, ph) { + app.get('/*', function (request, response) { + if ((request.url == '/') || (request.url == '/index.html')) { + if (request.headers['user-agent'].indexOf('PhantomJS') !== -1) { + response.sendfile(base + '/index.html'); + } else { + getThroughPhantom(true); + } + return; + } + response.sendfile(base + request.url, {maxAge: hourMs}, getThroughPhantom); + + function getThroughPhantom(error) { + if (!error) { + return; + } + if (cache) { + var cached = cache.get(request.url); + if (cached) { + response.status(200); + response.send(cached); + return; + } + } + + ph.createPage(function (err, page) { + var url = request.protocol + '://' + request.headers.host + '/#' + request.url; + var outputPage = function () { + page.evaluate((function () { + return document.documentElement.outerHTML; + }), function (err, result) { + links.length && response.setHeader('Link', links); + cache && cache.set(request.originalUrl, result); + response.status(200); + response.send(result); + clearTimeout(timeout); + page.close(); + return ph; + }); + }; - server.all(apiPrefix + '*', proxyRequest); - - server.get('/*', function(req, res) { - res.redirect(util.format('/#%s#', req.originalUrl)); - }); + page.onCallback = function (data) { + if (data.event && data.event == 'enterComplete') { + outputPage(); + } + }; - if (vhost) { - server.use(express.vhost(vhost, server)); + // Add resource hinting. + var links = []; + page.onResourceReceived = function(resourceRequest) { + if (resourceRequest.id == 1 || resourceRequest.stage != 'end') { + return; + } + links.push('<' + resourceRequest.url + '>; rel=subresource'); + } + + var timeout = setTimeout(outputPage.bind(this), 10000); // timeout in 10 seconds + + return page.open(url, function (err, status) { + //console.log('open', url, status); + err && console.log('page.open error:', err); + }); + }); + } + }); + listenUp(); + }, {parameters:{'ignore-ssl-errors':'yes'}}); // ! phantom.create + } else { + // dev mode. + app.use(express.static(base, {maxAge: hourMs})); + app.get('/*', function (req, res) { + res.redirect(util.format('/#%s#', req.originalUrl)); + }); + listenUp(); } - server.listen(port); - return server; - }; + config.livereload && livereload(app, {watchDir: base}); + + function listenUp() { + if (domain) { + app.use(vhost(domain, app)); + } + + if (config.ssl) { + var spdyOptions = { + key: fs.readFileSync(config.ssl.key), + cert: fs.readFileSync(config.ssl.cert), + ca: fs.readFileSync(config.ssl.ca) + }; + var spdyServer = spdy.createServer(spdyOptions, app); + spdyServer.listen(config.ssl.port); + console.log('SPDY server running at %s:%d', domain, config.ssl.port); + // set up a route to redirect http to https + var httpServer = express(); + httpServer.use('*', function(req,res){ + res.redirect('https://' + req.headers.host + (config.ssl.port == 443 ? '' : ':' + config.ssl.port) + req.url) + }); + domain && httpServer.use(vhost(domain, httpServer)); + httpServer.listen(port); + console.log('Express forwarder running at %s:%d', domain, port); + } else { + app.listen(port); + console.log('Express server running at %s:%d', domain, port); + } + } + return app; + }; - grunt.registerMultiTask('server', 'Runs a static web and proxy server', function() { - var options = this.options({ - }); - var server = startServer({ + grunt.registerMultiTask('server', 'Runs a static web and proxy server', function () { + var options = this.options({}); + var app = startServer({ host: options.apiBaseUrl, // override this with your third party API host, such as 'search.twitter.com'. - hourMs: 0*60*60, + hourMs: 0 * 60 * 60, vhost: options.vhost, base: options.base, port: options.port, apiPrefix: options.apiPrefix, proxyPort: options.proxyPort || '80', - proxyProtocol: options.proxyProtocol || 'http' - }), - args = this.args, - done = args[args.length-1] === 'watch' ? function() {} : this.async(); - - server.on('close', done); - - console.log('Express server running at %s:%d', options.vhost, options.port); + proxyProtocol: options.proxyProtocol || 'http', + preRender: options.preRender, + compress: options.compress, + livereload: options.livereload, + ssl: options.ssl + }), + args = this.args, + done = args[args.length - 1] === 'watch' ? function () { + } : this.async(); + app.on('close', done); }); };