├── README.md ├── es6.conf ├── less.conf ├── sass.conf ├── scss.conf └── test ├── test.es6 ├── test.less ├── test.sass ├── test.scss └── test_error.es6 /README.md: -------------------------------------------------------------------------------- 1 | # Server side processing with nginx/lua 2 | 3 | This whole thing started as a server-side LESS -> CSS converter. And then I added SASS and SCSS. 4 | I will not lie to myself and I think I will add a couple of other converters in time. 5 | 6 | Right now, the list is as follows: 7 | 8 | * LESS - using npm/less converter (`npm install -g less`) 9 | * SASS and SCSS - using ruby gem (`gem install sass`) 10 | * ES6 to ES5 - using npm/es6-transpiler or npm/babel (`npm install -g es6-transpiler/babel`) 11 | 12 | Feel free to suggest any other thing you'd like to convert on the server side. 13 | 14 | 15 | ## Note on ES6 transpiling 16 | 17 | Generally the transpilers are quite slow. A simple test file needs about 700-800ms on my dev server with es6-transpiler 18 | and babel performed worse, with about 1100ms. As such it is not *very* usable. Transpiler speeds may/will improve 19 | in the future, but until then be mindful about which transpiler you use, and try to stick with the faster ones. 20 | 21 | ## Caching 22 | 23 | The settings here don't perform any caching. Due to the nature of less/css/* files, it is also impossible to add a 24 | caching layer that would be fully aware of dependencies outside of your main entry file. In the case of less it would 25 | mean every other less source file, and image resources which could be loaded by mixins like `image-width`. 26 | The main strategy of the configs is to provide missing nginx CGI functionality via the LUA extension. As such, 27 | caching is not the main objective of this project and any output is generated on the fly. It is also a good reason 28 | why this shouldn't be used in production, but only development environments (where feasible). 29 | 30 | ## Motivation behind the project 31 | 32 | In general, writing LESS/SASS and ES6 gives you some aesthetic and practical benefits. 33 | An accepted practice is to generate the CSS/JS and other files, minify them and deploy them in production. 34 | 35 | __The same should apply also for development.__ 36 | 37 | I asume that some of you are generating your files with `lessc index.less > index.css` or something similar, and 38 | are looking for a better solution. You are very much familiar with the pain points of generating these files and 39 | what suffering it brings. You really should skip a few solutions which are better, but have other pitfalls. 40 | 41 | * Editors/IDE include build hooks to generate css from less/sass ([Sublime Text](https://github.com/timdouglas/sublime-less2css)) 42 | * Monitor file changes and run conversions ([Grunt](https://github.com/gruntjs/grunt-contrib-less)) 43 | * Client side dependencies like `less.js` take care of rendering less in the browser 44 | 45 | All of these are quite hack-ish ways to do what the server should be doing for you. You want to have your development 46 | environment as close as possible to the production environment. Sure, you will still generate css and javascript files 47 | in the production - but should you adapt your development process to include build hooks in your editors, client side 48 | dependencies which might be out of date, and generally make developers lives harder? 49 | 50 | The only difference between development and production in this case should be only the extension of the file 51 | you're loading in the browser. Load `.less`, `.sass` and other files in development, and load `.css` and other 52 | in production. 53 | 54 | 1. Only version and keep source files on disk (no generated files) 55 | 56 | Most tools just generate additional files in the same folders. This way main.less becomes main.css and so on. 57 | It causes valuable time to the developer to exclude these files from source control, and to pay attention not 58 | to open and edit the wrong file. 59 | 60 | 2. The files might differ from developer machine to other dev machine 61 | 62 | Converting the files on the server side actually simplifies the development process in teams. You can be sure 63 | that correct versions of software are being used, you can use server-side features not available in browsers, 64 | and you don't have to keep developers up to date with more and more client side dependencies. 65 | 66 | 3. Use advanced features not available in browsers 67 | 68 | With ES6 to ES5 transpiler it's possible to develop with next generation JavaScript language syntax. Using 69 | LESS you can use the `image-width` and other mixins which are not available in browsers. In theory you can 70 | use resources that are not available on the client side - most notably, databases. 71 | 72 | Server-side processing is nothing new. Perl has been doing it for years, and so has PHP, python, ruby and a plethora 73 | of other programming languages. Since there is a distinct lack of support for LESS, SASS and currently even ES6 74 | in any of the top-tier browsers, I see a solution for it by using the same approach as with programming languages. 75 | 76 | In practice, any script than converts X to Y can be run on server side. 77 | 78 | ## Ok, you convinced me - what do I need? 79 | 80 | You need: 81 | 82 | - nginx compiled with LUA support 83 | - lua-socket extension (not critical, can be excluded if you're willing to modify the configs) 84 | - converter scripts (less, sass, es6toes5,...) 85 | - various selection of the following: `node`, `npm`, `ruby`, `gem`. 86 | 87 | Use the provided less/sass/scss/es6.conf in nginx (copy it to `/etc/nginx/conf.d` perhaps), restart your nginx 88 | instance, and use less/scss/sass/es6 files in your browser, like you would with css files. Edit them, save them, 89 | and refresh your pages. 90 | 91 | ``` 92 | 93 | 94 | 95 | 96 | ``` 97 | 98 | ## Thanks 99 | 100 | You are great if you read all this instead of just using the code. I'm sorry for ranting so much. 101 | 102 | Send me an email/paypal at black@scene-si.org if you feel thankful. -------------------------------------------------------------------------------- /es6.conf: -------------------------------------------------------------------------------- 1 | location ~ \.es6$ { 2 | content_by_lua ' 3 | 4 | function file_exists(name) 5 | local f=io.open(name,"r") 6 | if f~=nil then io.close(f) return true else return false end 7 | end 8 | 9 | function strpos (haystack, needle, offset) 10 | local pattern = string.format("(%s)", needle) 11 | local i = string.find (haystack, pattern, (offset or 0)) 12 | return (i ~= nil and i or false) 13 | end 14 | 15 | function is_compiler_error(result) 16 | if (strpos(result, "Error")) then 17 | return true; 18 | end 19 | return false; 20 | end 21 | 22 | local filename = "/var/www/" .. ngx.var.vhost .. "/public_html" .. ngx.var.uri; 23 | if (file_exists(filename)) then 24 | 25 | local socket = require("socket"); 26 | 27 | local startTime = socket.gettime() * 1000; 28 | 29 | // local handle = io.popen("/usr/local/bin/babel " .. filename .. " 2>&1"); 30 | local handle = io.popen("/usr/local/lib/node_modules/es6-transpiler/build/es5/es6toes5 " .. filename .. " 2>&1"); 31 | local result = handle:read("*all"); 32 | handle:close(); 33 | 34 | local endTime = socket.gettime() * 1000; 35 | 36 | ngx.header["Content-type"] = "application/javascript; charset=utf-8"; 37 | ngx.header["Cache-Control"] = "no-cache, no-store, must-revalidate" 38 | ngx.header["Pragma"] = "no-cache" 39 | ngx.header["Expires"] = "0" 40 | 41 | if (not is_compiler_error(result)) then 42 | ngx.say("/* generated with nginx/lua " .. os.date("%Y-%m-%d %H:%M:%S") .. " */"); 43 | ngx.say("/* took " .. (endTime - startTime) .. "ms */"); 44 | ngx.say(""); 45 | ngx.say(result); 46 | else 47 | -- ngx.exit(404); 48 | ngx.say("/* generated with nginx/lua " .. os.date("%Y-%m-%d %H:%M:%S") .. " */"); 49 | ngx.say("/* took " .. (endTime - startTime) .. "ms */"); 50 | ngx.say(""); 51 | ngx.say("/*"); 52 | ngx.say(string.gsub(result, "^%s*(.-)%s*$", "%1")); 53 | ngx.say("*/"); 54 | end 55 | else 56 | ngx.say(filename); 57 | ngx.say("es6 file not found"); 58 | ngx.exit(404); 59 | end 60 | '; 61 | } -------------------------------------------------------------------------------- /less.conf: -------------------------------------------------------------------------------- 1 | location ~ \.less$ { 2 | content_by_lua ' 3 | 4 | function file_exists(name) 5 | local f=io.open(name,"r") 6 | if f~=nil then io.close(f) return true else return false end 7 | end 8 | 9 | function strpos (haystack, needle, offset) 10 | local pattern = string.format("(%s)", needle) 11 | local i = string.find (haystack, pattern, (offset or 0)) 12 | return (i ~= nil and i or false) 13 | end 14 | 15 | function is_compiler_error(result) 16 | if (strpos(result, "Error")) then 17 | return true; 18 | end 19 | return false; 20 | end 21 | 22 | local filename = "/var/www/" .. ngx.var.vhost .. "/public_html" .. ngx.var.uri; 23 | if (file_exists(filename)) then 24 | 25 | local socket = require("socket"); 26 | 27 | local startTime = socket.gettime() * 1000; 28 | 29 | local handle = io.popen("PATH=/usr/local/bin /usr/local/bin/lessc " .. filename .. " 2>&1"); 30 | local result = handle:read("*all"); 31 | handle:close(); 32 | 33 | local endTime = socket.gettime() * 1000; 34 | 35 | ngx.header["Content-type"] = "text/css; charset=utf-8"; 36 | ngx.header["Cache-Control"] = "no-cache, no-store, must-revalidate" 37 | ngx.header["Pragma"] = "no-cache" 38 | ngx.header["Expires"] = "0" 39 | 40 | if (not is_compiler_error(result)) then 41 | ngx.say("/* generated with nginx/lua " .. os.date("%Y-%m-%d %H:%M:%S") .. " */"); 42 | ngx.say("/* took " .. (endTime - startTime) .. "ms */"); 43 | ngx.say(""); 44 | ngx.say(result); 45 | else 46 | -- ngx.exit(404); 47 | ngx.say("/* generated with nginx/lua " .. os.date("%Y-%m-%d %H:%M:%S") .. " */"); 48 | ngx.say("/* took " .. (endTime - startTime) .. "ms */"); 49 | ngx.say(""); 50 | ngx.say("* { background-color: red !important; }"); 51 | ngx.say(""); 52 | ngx.say("/*"); 53 | ngx.say(string.gsub(result, "^%s*(.-)%s*$", "%1")); 54 | ngx.say("*/"); 55 | end 56 | else 57 | ngx.say(filename); 58 | ngx.say("less file not found"); 59 | ngx.exit(404); 60 | end 61 | '; 62 | } -------------------------------------------------------------------------------- /sass.conf: -------------------------------------------------------------------------------- 1 | location ~ \.sass$ { 2 | content_by_lua ' 3 | 4 | function file_exists(name) 5 | local f=io.open(name,"r") 6 | if f~=nil then io.close(f) return true else return false end 7 | end 8 | 9 | function strpos (haystack, needle, offset) 10 | local pattern = string.format("(%s)", needle) 11 | local i = string.find (haystack, pattern, (offset or 0)) 12 | return (i ~= nil and i or false) 13 | end 14 | 15 | function is_compiler_error(result) 16 | if (strpos(result, "Error")) then 17 | return true; 18 | end 19 | return false; 20 | end 21 | 22 | local filename = "/var/www/" .. ngx.var.vhost .. "/public_html" .. ngx.var.uri; 23 | if (file_exists(filename)) then 24 | 25 | local socket = require("socket"); 26 | 27 | local startTime = socket.gettime() * 1000; 28 | 29 | local handle = io.popen("PATH=/usr/local/bin /usr/local/bin/sass " .. filename .. " 2>&1"); 30 | local result = handle:read("*all"); 31 | handle:close(); 32 | 33 | local endTime = socket.gettime() * 1000; 34 | 35 | ngx.header["Content-type"] = "text/css; charset=utf-8"; 36 | ngx.header["Cache-Control"] = "no-cache, no-store, must-revalidate" 37 | ngx.header["Pragma"] = "no-cache" 38 | ngx.header["Expires"] = "0" 39 | 40 | if (not is_compiler_error(result)) then 41 | ngx.say("/* generated with nginx/lua " .. os.date("%Y-%m-%d %H:%M:%S") .. " */"); 42 | ngx.say("/* took " .. (endTime - startTime) .. "ms */"); 43 | ngx.say(""); 44 | ngx.say(result); 45 | else 46 | -- ngx.exit(404); 47 | ngx.say("/* generated with nginx/lua " .. os.date("%Y-%m-%d %H:%M:%S") .. " */"); 48 | ngx.say("/* took " .. (endTime - startTime) .. "ms */"); 49 | ngx.say(""); 50 | ngx.say("* { background-color: red !important; }"); 51 | ngx.say(""); 52 | ngx.say("/*"); 53 | ngx.say(string.gsub(result, "^%s*(.-)%s*$", "%1")); 54 | ngx.say("*/"); 55 | end 56 | else 57 | ngx.say(filename); 58 | ngx.say("sass file not found"); 59 | ngx.exit(404); 60 | end 61 | '; 62 | } -------------------------------------------------------------------------------- /scss.conf: -------------------------------------------------------------------------------- 1 | location ~ \.scss$ { 2 | content_by_lua ' 3 | 4 | function file_exists(name) 5 | local f=io.open(name,"r") 6 | if f~=nil then io.close(f) return true else return false end 7 | end 8 | 9 | function strpos (haystack, needle, offset) 10 | local pattern = string.format("(%s)", needle) 11 | local i = string.find (haystack, pattern, (offset or 0)) 12 | return (i ~= nil and i or false) 13 | end 14 | 15 | function is_compiler_error(result) 16 | if (strpos(result, "Error")) then 17 | return true; 18 | end 19 | return false; 20 | end 21 | 22 | local filename = "/var/www/" .. ngx.var.vhost .. "/public_html" .. ngx.var.uri; 23 | if (file_exists(filename)) then 24 | 25 | local socket = require("socket"); 26 | 27 | local startTime = socket.gettime() * 1000; 28 | 29 | local handle = io.popen("PATH=/usr/local/bin /usr/local/bin/sass --scss " .. filename .. " 2>&1"); 30 | local result = handle:read("*all"); 31 | handle:close(); 32 | 33 | local endTime = socket.gettime() * 1000; 34 | 35 | ngx.header["Content-type"] = "text/css; charset=utf-8"; 36 | ngx.header["Cache-Control"] = "no-cache, no-store, must-revalidate" 37 | ngx.header["Pragma"] = "no-cache" 38 | ngx.header["Expires"] = "0" 39 | 40 | if (not is_compiler_error(result)) then 41 | ngx.say("/* generated with nginx/lua " .. os.date("%Y-%m-%d %H:%M:%S") .. " */"); 42 | ngx.say("/* took " .. (endTime - startTime) .. "ms */"); 43 | ngx.say(""); 44 | ngx.say(result); 45 | else 46 | -- ngx.exit(404); 47 | ngx.say("/* generated with nginx/lua " .. os.date("%Y-%m-%d %H:%M:%S") .. " */"); 48 | ngx.say("/* took " .. (endTime - startTime) .. "ms */"); 49 | ngx.say(""); 50 | ngx.say("* { background-color: red !important; }"); 51 | ngx.say(""); 52 | ngx.say("/*"); 53 | ngx.say(string.gsub(result, "^%s*(.-)%s*$", "%1")); 54 | ngx.say("*/"); 55 | end 56 | else 57 | ngx.say(filename); 58 | ngx.say("scss file not found"); 59 | ngx.exit(404); 60 | end 61 | '; 62 | } -------------------------------------------------------------------------------- /test/test.es6: -------------------------------------------------------------------------------- 1 | class View { 2 | constructor(options) { 3 | this.options = options; 4 | } 5 | 6 | render() { 7 | return "whatever " + this.options.name; 8 | } 9 | } 10 | 11 | var a = new View({ name: "Tit Petric" }); 12 | console.log(a.render()); 13 | 14 | var b = new View({ name: "Anonymous" }); 15 | console.log(b.render()); -------------------------------------------------------------------------------- /test/test.less: -------------------------------------------------------------------------------- 1 | @primary-color: #333; 2 | 3 | nav { 4 | ul { 5 | margin: 0; 6 | padding: 0; 7 | list-style: none; 8 | } 9 | li { 10 | display: inline-block; 11 | color: @primary-color; 12 | } 13 | a { 14 | display: block; 15 | padding: 6px 12px; 16 | text-decoration: none; 17 | } 18 | } -------------------------------------------------------------------------------- /test/test.sass: -------------------------------------------------------------------------------- 1 | $primary-color: #333 2 | 3 | nav 4 | ul 5 | margin: 0 6 | padding: 0 7 | list-style: none 8 | 9 | li 10 | display: inline-block 11 | color: $primary-color 12 | 13 | a 14 | display: block 15 | padding: 6px 12px 16 | text-decoration: none 17 | -------------------------------------------------------------------------------- /test/test.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #333; 2 | 3 | nav { 4 | ul { 5 | margin: 0; 6 | padding: 0; 7 | list-style: none; 8 | } 9 | li { 10 | display: inline-block; 11 | color: $primary-color; 12 | } 13 | a { 14 | display: block; 15 | padding: 6px 12px; 16 | text-decoration: none; 17 | } 18 | } -------------------------------------------------------------------------------- /test/test_error.es6: -------------------------------------------------------------------------------- 1 | class View { 2 | constructor(options) { 3 | this.options = options; 4 | } 5 | 6 | render() { 7 | return "whatever " + this.options.name; 8 | } 9 | } 10 | 11 | var a = new View({ name: "Tit Petric" }); 12 | console.log(a.render())); 13 | 14 | var b = new View({ name: "Anonymous" }); 15 | console.log(b.render()); --------------------------------------------------------------------------------