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