diff --git a/.travis.yml b/.travis.yml
index 85ff964..691fdab 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,4 +10,6 @@ before_script:
- "export DISPLAY=:99.0"
script:
- - npm run postinstall-deploy -s
+ - npm run lint -s
+ - npm test
+ - npm run browsertest -s
diff --git a/README.md b/README.md
index 2b71eaa..057b901 100644
--- a/README.md
+++ b/README.md
@@ -22,29 +22,21 @@ LICENSE file.
4. If you want to use the backend, also install come2help-server. See the tutorial in come2help-server.
-5. Install the development dependencies:
+5. Install all dependencies:
npm install
-6. Use bower to install the dependencies:
- bower install
+6. Start the development services:
+ npm start
This will start:
+ * A watch to build all files that need to be built.
+ * A webserver
+ * A LiveReload server
-7. Run the index.html in a browser of your choice.
- Be careful about cross-site script detection, since the server is not running at the same domain as your client.
- Maybe you have to use a proxy server like nginx.
- If `style.css` is missing, run `npm run build-css` first to transpile SASS to CSS.
-
-8. Start a python Webserver with:
- python -m SimpleHTTPServer
- or
- python3 -m http.server
- No caching alternatives:
- python nocacheserver.py
- or
- python3 nocacheserver3.py
+7. Run `localhost:8000` in a browser of your choice. Install [LiveReload](http://livereload.com/) to have it automatically updated.
+8. Start coding!
## Testing
-Tests are run through the `npm` interface:
+Tests are run through `npm`:
* `npm test`
@@ -61,4 +53,3 @@ Tests are run through the `npm` interface:
* `npm run lint -s`
Lints the code.
-
diff --git a/nocacheserver.py b/nocacheserver.py
deleted file mode 100644
index 01d3f35..0000000
--- a/nocacheserver.py
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env python
-import SimpleHTTPServer
-
-class MyHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
- def end_headers(self):
- self.send_my_headers()
- SimpleHTTPServer.SimpleHTTPRequestHandler.end_headers(self)
-
- def send_my_headers(self):
- self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
- self.send_header("Pragma", "no-cache")
- self.send_header("Expires", "0")
-
-if __name__ == '__main__':
- SimpleHTTPServer.test(HandlerClass=MyHTTPRequestHandler)
diff --git a/nocacheserver3.py b/nocacheserver3.py
deleted file mode 100644
index 9d66e05..0000000
--- a/nocacheserver3.py
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/bin/env python
-import http.server
-
-class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
- def end_headers(self):
- self.send_my_headers()
- http.server.SimpleHTTPRequestHandler.end_headers(self)
-
- def send_my_headers(self):
- self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
- self.send_header("Pragma", "no-cache")
- self.send_header("Expires", "0")
-
-
-if __name__ == '__main__':
- http.server.test(HandlerClass=MyHTTPRequestHandler)
diff --git a/package.json b/package.json
index 6d6af7d..87e7eef 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"chai": "^3.3.0",
"chai-as-promised": "^5.1.0",
"closurecompiler": "^1.5.2",
+ "colors": "^1.1.2",
"connect": "^3.4.0",
"date-format-lite": "^0.7.4",
"eslint": "^1.6.0",
@@ -40,22 +41,23 @@
"eslint-plugin-angular": "^0.12.0",
"extend": "^3.0.0",
"js-beautify": "^1.5.10",
+ "livereload": "^0.4.0",
"mocha": "^2.3.3",
"node-sass": "^3.3.3",
"portfinder": "^0.4.0",
"protractor": "git+https://github.com/jGleitz/protractor.git#browserstack",
"q": "^1.4.1",
- "serve-static": "^1.10.0"
+ "serve-static": "^1.10.0",
+ "watch": "^0.16.0"
},
"scripts": {
+ "postinstall": "bower install && npm run install-testdependencies && npm run build-css",
+ "build-css": "node tasks/sass",
"test": "protractor test/behaviour/protractor.conf",
- "chrometest": "protractor test/behaviour/protractor.conf --browser chrome",
+ "install-testdependencies": "webdriver-manager update --standalone --chrome --browserstacklocal",
"browsertest": "protractor test/behaviour/protractor.browserstack.conf",
- "postinstall": "webdriver-manager update --standalone --chrome --browserstacklocal && npm run build-css",
- "postinstall-deploy": "npm run lint -s && npm test -s && npm run browsertest -s",
- "postinstall-dev": "npm run lint -s && npm run watch-css -s",
- "build-css": "node-sass css --output build/css",
- "watch-css": "node-sass -w -r css --output build/css",
- "lint": "eslint js/**/*.js test/**/*.js"
+ "chrometest": "protractor test/behaviour/protractor.conf --browser chrome",
+ "lint": "eslint js/**/*.js test/**/*.js",
+ "start": "node tasks/watch"
}
}
diff --git a/tasks/.eslintrc b/tasks/.eslintrc
new file mode 100644
index 0000000..4640970
--- /dev/null
+++ b/tasks/.eslintrc
@@ -0,0 +1,30 @@
+{
+ "rules": {
+ "indent": [
+ 2,
+ "tab", {
+ "SwitchCase": 1
+ }
+ ],
+ "quotes": [
+ 2,
+ "single"
+ ],
+ "linebreak-style": [
+ 2,
+ "unix"
+ ],
+ "semi": [
+ 2,
+ "always"
+ ],
+ "no-mixed-spaces-and-tabs": [
+ 2, "smart-tabs"
+ ]
+ },
+ "env": {
+ "es6": true,
+ "node": true
+ },
+ "extends": "eslint:recommended"
+}
\ No newline at end of file
diff --git a/tasks/.jsbeautifyrc b/tasks/.jsbeautifyrc
new file mode 100644
index 0000000..a4656a0
--- /dev/null
+++ b/tasks/.jsbeautifyrc
@@ -0,0 +1,22 @@
+{
+ "indent_size": 2,
+ "indent_char": " ",
+ "eol": "\n",
+ "indent_level": 0,
+ "indent_with_tabs": true,
+ "preserve_newlines": true,
+ "max_preserve_newlines": 3,
+ "jslint_happy": false,
+ "space_after_anon_function": false,
+ "brace_style": "collapse",
+ "keep_array_indentation": false,
+ "keep_function_indentation": false,
+ "space_before_conditional": true,
+ "break_chained_methods": false,
+ "eval_code": false,
+ "unescape_strings": false,
+ "wrap_line_length": 0,
+ "wrap_attributes": "auto",
+ "wrap_attributes_indent_size": 4,
+ "end_with_newline": false
+}
\ No newline at end of file
diff --git a/tasks/sass.js b/tasks/sass.js
new file mode 100644
index 0000000..d44c304
--- /dev/null
+++ b/tasks/sass.js
@@ -0,0 +1,118 @@
+/**
+ * Handles SCSS rendering.
+ */
+
+
+var sass = require('node-sass');
+var path = require('path');
+var fs = require('fs');
+var q = require('q');
+
+var basePath = path.resolve(__dirname, '..');
+var resolve = path.resolve.bind(path, basePath);
+
+function returnQ(argument) {
+ return q.bind(q, argument);
+}
+
+/**
+ * Renders the given scss file.
+ *
+ * @param {string} filePath The path to .scss file out of the css folder.
+ * @return {promise} A promise that will be fulfilled with the path to the rendered file when it was rendered.
+ */
+function renderFile(filePath) {
+ var outFilePath = getOutputFilePath(filePath);
+ return q.nfcall(sass.render, {
+ file: filePath,
+ outFile: outFilePath
+ }).then(function(result) {
+ return mkdirs(path.dirname(outFilePath))
+ .then(q.nbind(fs.writeFile, fs, outFilePath, result.css));
+ }).then(returnQ(outFilePath));
+}
+
+function getOutputFilePath(filePath) {
+ filePath = filePath.replace(/.scss$/, '.css');
+ return resolve('build', path.relative(resolve(''), filePath));
+}
+
+/**
+ * Remove the rendered equivalent of the provided scss file.
+ *
+ * @param {string} filePath The path of a scss file that was present in the css folder, but was deleted.
+ * @return {promise} A promise that will be fulfilled with the path of the rendered file when it was deleted.
+ */
+function clean(filePath) {
+ var rendered = getOutputFilePath(filePath);
+ return q.nfcall(fs.unlink, rendered).then(returnQ(rendered));
+}
+
+module.exports = {
+ render: renderFile,
+ clean: clean
+};
+
+/**
+ * @param {string} dir A directory
+ * @return {promise} A promise that will be fulfilled when all directories on the way to dir (including dir itself) exist.
+ */
+function mkdirs(dir) {
+ return q.nfcall(fs.stat, dir).catch(function(error) {
+ if (error.code === 'ENOENT') {
+ return mkdirs(path.dirname(dir))
+ .then(q.nbind(fs.mkdir, fs, dir))
+ .catch(function(error) {
+ if (error.code !== 'EEXIST') {
+ throw error;
+ }
+ });
+ } else {
+ throw error;
+ }
+ });
+}
+
+/**
+ * @param {string} dir A directory
+ * @return {promise>} A promise for all files in that directory, including nested files.
+ */
+function walk(dir) {
+ var results = [];
+ return q.nfcall(fs.readdir, dir).then(function(list) {
+
+ if (list.length === 0) {
+ return list;
+ }
+
+ var promises = list.map(function(file) {
+ file = path.resolve(dir, file);
+
+ return q.nfcall(fs.stat, file).then(function(stat) {
+ if (stat.isDirectory()) {
+ return walk(file).then(function(res) {
+ results = results.concat(res);
+ });
+ } else {
+ results.push(file);
+ return q();
+ }
+ });
+ });
+ return q.all(promises).then(function() {
+ return results;
+ });
+ });
+}
+
+if (require.main === module) {
+ walk(resolve('css')).then(function(list) {
+ var promises = list.map(function(file) {
+ if (path.extname(file) === '.scss') {
+ return renderFile(file);
+ }
+ return q();
+ });
+ return q.all(promises);
+ }).done(process.exit.bind(process, 0));
+}
\ No newline at end of file
diff --git a/tasks/server.js b/tasks/server.js
new file mode 100644
index 0000000..9c5eb07
--- /dev/null
+++ b/tasks/server.js
@@ -0,0 +1,50 @@
+/**
+ * Very simple webserver for development purposes.
+ */
+
+var connect = require('connect');
+var serveStatic = require('serve-static');
+var path = require('path');
+
+var baseDir = path.resolve(__dirname, '..');
+
+function getWebserver() {
+ var setHeaders = function(res) {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ };
+ return serveStatic(baseDir, {
+ setHeaders: setHeaders,
+ etag: false
+ });
+}
+
+/**
+ * A simple web server that will default to deliver the base directory of this repository. It can be extended through #use.
+ */
+function Server() {
+
+ var server = connect();
+
+ /**
+ * Use the provided middleware. The order of the calls defines the order in which middlewares will be queried.
+ *
+ * @param {Object} server A node js server middleware.
+ * @return {void}
+ */
+ this.use = server.use.bind(server);
+
+ /**
+ * Launch the server and listen on the provided port.
+ *
+ * @param {number} port The port to listen on.
+ * @return {void}
+ */
+ this.listen = function(port) {
+ server.use(getWebserver());
+ server.listen(port);
+ };
+}
+
+module.exports = Server;
\ No newline at end of file
diff --git a/tasks/watch.js b/tasks/watch.js
new file mode 100644
index 0000000..f4b5d68
--- /dev/null
+++ b/tasks/watch.js
@@ -0,0 +1,132 @@
+/**
+ * Starts the development environment.
+ */
+
+var livereload = require('livereload');
+var path = require('path');
+var Server = require('./server');
+var watch = require('watch');
+var sass = require('./sass');
+var util = require('util');
+require('colors');
+
+var basePath = path.resolve(__dirname, '..');
+
+/**
+ * Resolve part relative to the repository base path.
+ *
+ * @param {string} part A relative path in this repository.
+ * @return {string} An absolute path to part.
+ */
+var resolve = path.resolve.bind(path, basePath);
+
+/**
+ * Make path relative to the repository base path.
+ *
+ * @param {string} path An absolute path pointing into this repository.
+ * @return {string} The relative path in this repository.
+ */
+var relative = path.relative.bind(path, basePath);
+
+/**
+ * Short for process.stdout.write
+ */
+var write = process.stdout.write.bind(process.stdout);
+
+/**
+ * Creates a success report handler.
+ *
+ * @param {string} input The message to print on success. If it contains format strings, these will be filled with the result passed to the handler.
+ * @param {boolean=} path If true, the result is an absolute path in this repository and will be made relative by calling relative.
+ * @return {void}
+ */
+var success = function(input, path) {
+ return function(result) {
+ var output = util.format.bind(util, input).apply(util, path ? relative(result) : result);
+ writeln(output.green);
+ writeln('');
+ };
+};
+
+/**
+ * Calls write and appends '\n'
+ */
+var writeln = function(input) {
+ write(input + '\n');
+};
+
+/**
+ * Prints 'OK\n' in green
+ */
+var ok = function() {
+ writeln('OK'.green);
+};
+
+
+
+// Watch
+write('Initializing file watch... ');
+watch.createMonitor(resolve('css'), handleCss);
+ok();
+
+// Live reload
+write('Launching the live reload server... ');
+var liverloadserver = livereload.createServer();
+liverloadserver.watch([resolve('build'), resolve('js'), resolve('partials'), resolve('index.html')]);
+ok();
+
+
+// Web server
+write('Launching the webserver... ');
+var webserver = new Server();
+webserver.listen(8000);
+ok();
+
+writeln('');
+
+function reportChange(monitor) {
+ monitor.on('created', defaultHandler('CREATE'));
+ monitor.on('changed', defaultHandler('CHANGE'));
+ monitor.on('removed', defaultHandler('REMOVE'));
+}
+
+function defaultHandler(action) {
+ return function(file) {
+ writeln((action + ': ' + relative(file)).grey);
+ };
+}
+
+
+function handleCss(monitor) {
+ reportChange(monitor);
+
+ monitor.on('created', renderSassFile);
+ monitor.on('changed', renderSassFile);
+
+ monitor.on('removed', function(file) {
+ if (path.extname(file) === '.scss') {
+ sass.clean(file).then(success('Cleaned %s', true)).catch(reportError);
+ }
+ });
+}
+
+function renderSassFile(file) {
+ if (path.extname(file) === '.scss') {
+ sass.render(file).then(success('Rendered to %s', true)).catch(reportError);
+ }
+}
+
+
+function reportError(error) {
+ write('Error: '.red);
+ if (error.message) {
+ writeln(error.message.red);
+ writeln('');
+ writeln(error.stack.gray);
+ } else if (typeof error === 'string') {
+ writeln(error.red);
+ } else {
+ writeln(util.inspect(error).red);
+ }
+ writeln('');
+}
\ No newline at end of file
diff --git a/test/behaviour/setup.js b/test/behaviour/setup.js
index a27f3b1..152bc52 100644
--- a/test/behaviour/setup.js
+++ b/test/behaviour/setup.js
@@ -2,9 +2,9 @@
* Sets up the test environment
*/
-var path = require('path');
var mocker = require('./apimocker');
var portfinder = require('portfinder');
+var Server = require('../../tasks/server');
var port;
var host = 'localhost';
@@ -31,10 +31,7 @@ before('Expose globals', function() {
});
before('Set up the JSON server and mocker', function() {
- var connect = require('connect');
- var serveStatic = require('serve-static');
- var server = connect();
+ var server = new Server();
server.use(mocker.middleware);
- server.use(serveStatic(path.resolve(__dirname, '../..')));
server.listen(port);
});
\ No newline at end of file