├── .gitignore ├── examples ├── style │ ├── link.css │ ├── import.css │ └── link.less ├── turtle.jpg ├── helloworld.jsx ├── index.html ├── less.html ├── prefixfree.html ├── processImage.html ├── processImage.css └── js │ ├── prefixfree.min.js │ └── less-1.1.5.min.js ├── test ├── proxy │ ├── ba r │ │ └── hello.txt │ ├── fo o │ │ └── hello.txt │ ├── noheadnoendhtml.html │ ├── nohead.html │ ├── subdir │ │ └── index.html │ ├── index.html │ └── earlyscript.html ├── nonProxy │ ├── ba r │ │ └── hello.txt │ ├── fo o │ │ └── hello.txt │ ├── style sheet.css │ └── index.html ├── middlewares │ ├── main.jsx │ ├── main.css │ ├── main.less │ ├── main.scss │ └── index.html ├── compilessAutoprefixer │ └── test.less ├── createLiveStyleTestServer.js ├── proxy-test.js └── nonProxy-test.js ├── .travis.yml ├── css.js ├── LICENSE ├── lib ├── middleware │ ├── bufferDataEventsUntilFirstListener.js │ └── injectLiveStyleScriptIncludeIntoHtml.js ├── createLiveStyleApp.js └── installLiveCssFileWatcherInServer.js ├── package.json ├── less.js ├── bin └── livestyle ├── README.md └── livestyle-client.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /examples/style/link.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /test/proxy/ba r/hello.txt: -------------------------------------------------------------------------------- 1 | The contents of /ba r/hello.txt 2 | -------------------------------------------------------------------------------- /test/proxy/fo o/hello.txt: -------------------------------------------------------------------------------- 1 | The contents of /foo/hello.txt 2 | -------------------------------------------------------------------------------- /test/nonProxy/ba r/hello.txt: -------------------------------------------------------------------------------- 1 | The contents of /ba r/hello.txt 2 | -------------------------------------------------------------------------------- /test/nonProxy/fo o/hello.txt: -------------------------------------------------------------------------------- 1 | The contents of /fo o/hello.txt 2 | -------------------------------------------------------------------------------- /examples/style/import.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: black; 3 | } 4 | -------------------------------------------------------------------------------- /test/nonProxy/style sheet.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: red; 3 | } 4 | -------------------------------------------------------------------------------- /examples/style/link.less: -------------------------------------------------------------------------------- 1 | @color: #FFF; 2 | 3 | h1 { 4 | color: @color; 5 | } 6 | -------------------------------------------------------------------------------- /test/middlewares/main.jsx: -------------------------------------------------------------------------------- 1 | define(function (require) { 2 | return 'module'; 3 | }); 4 | -------------------------------------------------------------------------------- /examples/turtle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/podio/podio-livestyle/master/examples/turtle.jpg -------------------------------------------------------------------------------- /test/middlewares/main.css: -------------------------------------------------------------------------------- 1 | .css { 2 | background: red; 3 | 4 | transform: translateZ(2); 5 | } 6 | -------------------------------------------------------------------------------- /test/proxy/noheadnoendhtml.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | Hello, upstream world! 5 | 6 | -------------------------------------------------------------------------------- /test/proxy/nohead.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello, upstream world! 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/helloworld.jsx: -------------------------------------------------------------------------------- 1 | React.renderComponent( 2 |Launch livestyle --processimage and play with the 9 | processing instructions in the image urls 10 | in processImage.css
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/processImage.css: -------------------------------------------------------------------------------- 1 | .turtle { 2 | float: left; 3 | width: 481px; 4 | height: 424px; 5 | } 6 | 7 | .first { 8 | background-image: url(turtle.jpg); 9 | } 10 | 11 | .second { 12 | background-image: url(turtle.jpg?jpegtran=-flip,horizontal); 13 | } 14 | 15 | .third { 16 | background-image: url(turtle.jpg?jpegtran=-grayscale); 17 | } 18 | 19 | .fourth { 20 | background-image: url(turtle.jpg?setFormat=png&pngquant=16); 21 | } 22 | 23 | .thesvg { 24 | float: left; 25 | border: 1px solid red; 26 | width: 481px; 27 | height: 424px; 28 | background-image: url(network-error.svg?svgfilter=--runScript,--stopColor=beige&inkscape=--export-png); 29 | background-color: red; 30 | } 31 | -------------------------------------------------------------------------------- /test/createLiveStyleTestServer.js: -------------------------------------------------------------------------------- 1 | var createLiveStyleApp = require('../lib/createLiveStyleApp'); 2 | 3 | module.exports = function createLiveStyleTestServer(options) { 4 | var app = createLiveStyleApp(options); 5 | 6 | options.silentSocketIo = true; 7 | 8 | // Listen on a vacant TCP port and hand back the url + app 9 | var server = app.listen(0); 10 | 11 | require('../lib/installLiveCssFileWatcherInServer')(server, options, require('socket.io')); 12 | 13 | var address = server.address(); 14 | return { 15 | hostname: address.address, 16 | port: address.port, 17 | host: address.address + ':' + address.port, 18 | url: 'http://' + address.address + ':' + address.port, 19 | app: app 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /css.js: -------------------------------------------------------------------------------- 1 | define({ 2 | load: function (name, req, load, config, map) { 3 | var url = name, 4 | path = url.split('/'), 5 | linkElement = document.createElement('link'), 6 | headElement = document.getElementsByTagName('head')[0], 7 | parentModuleName = map && map.parentMap && map.parentMap.name, 8 | parentScriptElement, 9 | i, 10 | childNode; 11 | 12 | linkElement.setAttribute('href', url); 13 | linkElement.setAttribute('rel', 'stylesheet'); 14 | 15 | if (parentModuleName) { 16 | for (i = 0 ; i < headElement.childNodes.length ; i += 1) { 17 | childNode = headElement.childNodes[i]; 18 | if (childNode.nodeType === 1 && childNode.nodeName.toLowerCase() === 'script' && childNode.getAttribute('data-requiremodule') === parentModuleName) { 19 | parentScriptElement = childNode; 20 | break; 21 | } 22 | } 23 | } 24 | 25 | if (parentScriptElement) { 26 | headElement.insertBefore(linkElement, parentScriptElement); 27 | } else { 28 | headElement.appendChild(linkElement); 29 | } 30 | load(); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Peter Müller and Andreas Lind Petersen 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of the author nor the names of contributors may 15 | be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /lib/middleware/bufferDataEventsUntilFirstListener.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Middleware that buffers up the request's 'data' and 'end' events 3 | * until another 'data' listener is added, then replay the events in 4 | * order. 5 | * 6 | * Intended for use with formidable in scenarios where the IncomingForm 7 | * is initialized in a route after something async has happened 8 | * (authentication via a socket, for instance). 9 | */ 10 | 11 | function createEventBufferer(eventName, bufferedEvents) { 12 | return function () { // ... 13 | bufferedEvents.push([eventName].concat(Array.prototype.slice.call(arguments))); 14 | }; 15 | } 16 | 17 | var eventNamesToBuffer = ['data', 'end']; 18 | 19 | module.exports = function () { 20 | return function bufferDataEventsUntilFirstListener(req, res, next) { 21 | var eventBufferersByEventName = {}, 22 | bufferedEvents = []; 23 | eventNamesToBuffer.forEach(function (eventName) { 24 | var eventBufferer = createEventBufferer(eventName, bufferedEvents); 25 | req.on(eventName, eventBufferer); 26 | eventBufferersByEventName[eventName] = eventBufferer; 27 | }); 28 | req.on('newListener', function (name, listener) { 29 | if (name === 'data') { 30 | eventNamesToBuffer.forEach(function (eventName) { 31 | req.removeListener(eventName, eventBufferersByEventName[eventName]); 32 | }); 33 | process.nextTick(function () { 34 | bufferedEvents.forEach(function (bufferedEvent) { 35 | req.emit.apply(req, bufferedEvent); 36 | }); 37 | }); 38 | } 39 | }); 40 | next(); 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributors": [ 3 | { 4 | "name": "Peter Müller", 5 | "email": "munter@fumle.dk" 6 | }, 7 | { 8 | "name": "Andreas Lind", 9 | "email": "andreas@one.com" 10 | }, 11 | { 12 | "name": "Gustav Nikolaj Olsen", 13 | "email": "gustavnikolaj@gmail.com" 14 | } 15 | ], 16 | "name": "livestyle", 17 | "description": "NodeJS middleware and binary for setting up a webserver that notifies the browser of CSS updates. Polling fallback for pure client side functionality.", 18 | "version": "3.0.0", 19 | "homepage": "https://github.com/One-com/livestyle", 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/One-com/livestyle.git" 23 | }, 24 | "engines": { 25 | "node": ">= 0.10.0" 26 | }, 27 | "dependencies": { 28 | "errorhandler": "1.4.2", 29 | "express": "4.13.3", 30 | "express-autoprefixer": "4.0.3", 31 | "express-compile-sass": "^4.0.0", 32 | "express-compiless": "3.0.4", 33 | "express-jsxtransform": "4.0.2", 34 | "express-processimage": "10.2.3", 35 | "hijackresponse": "1.0.1", 36 | "morgan": "1.6.1", 37 | "optimist": "0.6.1", 38 | "request": "2.9.3", 39 | "socket.io": "2.0.4", 40 | "socket.io-client": "2.0.4" 41 | }, 42 | "directories": { 43 | "bin": "./bin" 44 | }, 45 | "main": "./lib/createLiveStyleApp.js", 46 | "devDependencies": { 47 | "autoprefixer": "^6.0.0", 48 | "mocha": "5.0.0", 49 | "node-sass": "^4.7.2", 50 | "postcss": "^5.0.0", 51 | "react-tools": "0.13.3", 52 | "unexpected": "10.37.2", 53 | "unexpected-express": "10.0.0" 54 | }, 55 | "scripts": { 56 | "test": "mocha --exit", 57 | "travis": "mocha --exit -R spec" 58 | }, 59 | "files": [ 60 | "livestyle-client.js", 61 | "css.js", 62 | "less.js", 63 | "lib", 64 | "bin" 65 | ], 66 | "preferGlobal": true, 67 | "license": "BSD-3-Clause" 68 | } 69 | -------------------------------------------------------------------------------- /less.js: -------------------------------------------------------------------------------- 1 | /*global less, console*/ 2 | define([ 3 | 'css' 4 | ], function (css) { 5 | var lessLoader = { 6 | load: function (name, req, load, config) { 7 | var url = name + (name.indexOf('.less') === -1 ? '.less' : ''), 8 | path = url.split('/'); 9 | 10 | path.pop(); 11 | if (path.length !== 0) { 12 | path = '/' + path.join('/') + '/'; 13 | } 14 | 15 | $.ajax({ 16 | url: url, 17 | dataType: 'text', 18 | success: function (data) { 19 | var parser = new less.Parser({ 20 | paths: [path] 21 | }); 22 | 23 | parser.parse(data, function (e, tree) { 24 | var style = null, 25 | css = null, 26 | lessLink; 27 | 28 | if (e) { 29 | console.error(e); 30 | } else { 31 | style = document.createElement('style'); 32 | style.type = "text/css"; 33 | style.id = 'less:' + url.replace(/\//g, '-').replace('.less', ''); 34 | 35 | css = tree.toCSS(); 36 | 37 | if (style.styleSheet) { 38 | style.styleSheet.cssText = css; 39 | } else { 40 | style.innerHTML = css; 41 | } 42 | document.getElementsByTagName('head')[0].appendChild(style); 43 | 44 | lessLink = document.createElement('link'); 45 | lessLink.rel = 'stylesheet/less'; 46 | lessLink.type = 'text/css'; 47 | lessLink.href = url; 48 | document.getElementsByTagName('head')[0].appendChild(lessLink); 49 | less.sheets.push(lessLink); 50 | 51 | load(css); 52 | } 53 | }); 54 | } 55 | }); 56 | } 57 | }; 58 | 59 | if (window.liveStyle && window.liveStyle.compiless) { 60 | lessLoader = css; 61 | } 62 | 63 | return lessLoader; 64 | }); 65 | -------------------------------------------------------------------------------- /examples/js/prefixfree.min.js: -------------------------------------------------------------------------------- 1 | // StyleFix 1.0.1 & PrefixFree 1.0.4 / by Lea Verou / MIT license 2 | (function(){function h(a,b){return[].slice.call((b||document).querySelectorAll(a))}if(window.addEventListener){var b=window.StyleFix={link:function(a){try{if(!/\bstylesheet\b/i.test(a.rel)||!a.sheet.cssRules)return}catch(c){return}var d=a.href||a.getAttribute("data-href"),f=d.replace(/[^\/]+$/,""),g=a.parentNode,e=new XMLHttpRequest;e.open("GET",d);e.onreadystatechange=function(){if(4===e.readyState){var c=e.responseText;if(c&&a.parentNode){c=b.fix(c,!0,a);f&&(c=c.replace(/url\((?:'|")?(.+?)(?:'|")?\)/gi, 3 | function(a,c){return!/^([a-z]{3,10}:|\/|#)/i.test(c)?'url("'+f+c+'")':a}),c=c.replace(RegExp("\\b(behavior:\\s*?url\\('?\"?)"+f,"gi"),"$1"));var d=document.createElement("style");d.textContent=c;d.media=a.media;d.disabled=a.disabled;d.setAttribute("data-href",a.getAttribute("href"));g.insertBefore(d,a);g.removeChild(a)}}};e.send(null);a.setAttribute("data-inprogress","")},styleElement:function(a){var c=a.disabled;a.textContent=b.fix(a.textContent,!0,a);a.disabled=c},styleAttribute:function(a){var c= 4 | a.getAttribute("style"),c=b.fix(c,!1,a);a.setAttribute("style",c)},process:function(){h('link[rel~="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link);h("style").forEach(StyleFix.styleElement);h("[style]").forEach(StyleFix.styleAttribute)},register:function(a,c){(b.fixers=b.fixers||[]).splice(void 0===c?b.fixers.length:c,0,a)},fix:function(a,c){for(var d=0;d