├── 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());
--------------------------------------------------------------------------------