├── template ├── .gitignore ├── server │ └── plugins │ │ └── HapiWebpackMiddleware │ │ ├── package.json │ │ └── index.js ├── .editorconfig ├── test │ ├── unit │ │ ├── .eslintrc │ │ ├── index.js │ │ ├── specs │ │ │ └── App.spec.js │ │ └── karma.conf.js │ └── api │ │ └── spec │ │ └── apiCall.js ├── .babelrc ├── client │ ├── main.js │ └── App.vue ├── build │ └── index_dev.html ├── .eslintrc.js ├── README.md ├── gulpfile.js ├── config │ ├── production.webpack.config.js │ └── webpack.config.js ├── app.js └── package.json ├── meta.js └── README.md /template/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /template/server/plugins/HapiWebpackMiddleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-webpack-middleware" 3 | } 4 | -------------------------------------------------------------------------------- /template/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /template/test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "expect": true, 7 | "sinon": true 8 | }, 9 | "rules": { 10 | "no-unused-expressions": "off" // https://github.com/eslint/eslint/issues/2102 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /template/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", { 4 | "modules": false 5 | }], 6 | "stage-2" 7 | ], 8 | "plugins": ["transform-runtime"], 9 | "comments": false, 10 | "env": { 11 | "test": { 12 | "plugins": ["istanbul"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /template/client/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable*/ 2 | 3 | import Vue from 'vue' 4 | import App from './App.vue' 5 | 6 | // Axios doesn't include Promise polyfill (e.g. 'es6-promise'), if you need one, here is a good place to initiate it 7 | 8 | new Vue({ 9 | el: '#app', 10 | components: { 11 | App 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /template/build/index_dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ name }} 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /template/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 4 | extends: 'standard', 5 | // required to lint *.vue files 6 | plugins: [ 7 | 'html' 8 | ], 9 | // add your custom rules here 10 | 'rules': { 11 | // allow paren-less arrow functions 12 | 'arrow-parens': 0, 13 | // allow debugger during development 14 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 15 | // require semicolons 16 | 'semi': ['error', 'always'] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /template/test/unit/index.js: -------------------------------------------------------------------------------- 1 | {{#if_eq headlessBrowser "PhantomJS"}} 2 | // Polyfill fn.bind() for PhantomJS 3 | /* eslint-disable no-extend-native */ 4 | Function.prototype.bind = require('function-bind'); 5 | {{/if_eq}} 6 | 7 | // require all test files (files that ends with .spec.js) 8 | var testsContext = require.context('./specs', true, /\.spec$/); 9 | testsContext.keys().forEach(testsContext); 10 | 11 | // require all src files except main.js for coverage. 12 | // you can also change this to match only the subset of files that 13 | // you want coverage for. 14 | var srcContext = require.context('../../client', true, /^\.\/(?!main(\.js)?$)/); 15 | srcContext.keys().forEach(srcContext); 16 | -------------------------------------------------------------------------------- /template/test/api/spec/apiCall.js: -------------------------------------------------------------------------------- 1 | var Chai = require('chai'); // assertion library 2 | var Lab = require('lab'); 3 | var lab = exports.lab = Lab.script(); 4 | 5 | // environment 6 | var describe = lab.describe; 7 | var it = lab.it; 8 | var before = lab.before; 9 | var beforeEach = lab.beforeEach; 10 | var expect = Chai.expect; 11 | 12 | var server = require('../../../app.js'); 13 | 14 | describe('apiCall', function () { 15 | it('should reply with "Hello!" messsage', async function () { 16 | var options = { 17 | method: 'GET', 18 | url: '/api/call' 19 | }; 20 | 21 | var response = await server.inject(options); 22 | 23 | expect(response).to.be.an('object'); 24 | expect(response.statusCode).to.be.equal(200); 25 | expect(response.result.message).to.be.equal('Hello!'); 26 | }); 27 | }); 28 | 29 | -------------------------------------------------------------------------------- /template/client/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 35 | 36 | 41 | -------------------------------------------------------------------------------- /template/README.md: -------------------------------------------------------------------------------- 1 | # {{ name }} 2 | 3 | {{ description }} 4 | 5 | ## Mini-doc 6 | 7 | **/build/** - Contains files needed for build and hot development 8 | **/build/index_dev.html** - Template for index.html, it will be used by HMR when developing in memory and during production build 9 | **/client/** - Vue.js app source 10 | **/config/** - Configuration files 11 | **/public/** - Public folder served by Hapi 12 | **/server/** - Server side logic 13 | **/test/** - Contains test files 14 | **/app.js** - Production server 15 | **/gulpfile.js** - Gulp setup file 16 | 17 | ## Dev Setup 18 | 19 | ``` bash 20 | # install dependencies 21 | npm install 22 | 23 | {{#unit}} 24 | # run unit tests 25 | npm run unit 26 | {{/unit}} 27 | 28 | {{#unitApi}} 29 | # run api unit tests 30 | npm run unit-api 31 | {{/unitApi}} 32 | 33 | # serve with hot reload at localhost:4000 (proxy from localhost:3000) 34 | npm run dev 35 | ``` 36 | 37 | ## Production Setup 38 | 39 | ``` bash 40 | # install dependencies 41 | npm install 42 | 43 | # build for production with minification 44 | npm run build 45 | 46 | # run application at localhost:3000 47 | node app.js 48 | ``` 49 | 50 | Credits: 51 | [Vue](https://vuejs.org/) 52 | [Hapi](http://hapijs.com/) 53 | [Gulp](https://gulpjs.com/) 54 | [BrowserSync](https://www.browsersync.io/) 55 | [Webpack](https://webpack.js.org/) 56 | -------------------------------------------------------------------------------- /template/test/unit/specs/App.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from 'client/App'; 3 | import axios from 'axios'; 4 | 5 | const appInstance = new Vue(App); 6 | 7 | describe('App.vue component', function () { 8 | it('should set correct default data', function () { 9 | expect(typeof App.data).to.be.equal('function'); 10 | var defaultData = App.data(); 11 | expect(defaultData.msg).to.be.equal('Welcome!'); 12 | }); 13 | 14 | let promiseCall; 15 | 16 | before(function () { 17 | promiseCall = sinon.stub(axios, 'get').returnsPromise(); 18 | }); 19 | 20 | after(function () { 21 | axios.get.restore(); 22 | }); 23 | 24 | it('should contain proper methods', function (done) { 25 | expect(typeof appInstance.helloCall).to.be.equal('function'); 26 | done(); 27 | }); 28 | 29 | // https://github.com/substantial/sinon-stub-promise 30 | it('helloCall should set proper data from AJAX response [success]', function (done) { 31 | promiseCall.resolves({ 32 | data: { 33 | message: 'Hello!' 34 | } 35 | }); 36 | appInstance.helloCall(); 37 | expect(appInstance.api).to.be.equal('Hello!'); 38 | done(); 39 | }); 40 | 41 | it('helloCall should set proper data from AJAX response [fail]', function (done) { 42 | promiseCall.rejects({ 43 | data: { 44 | 'statusCode': 400, 45 | 'error': 'Bad Request', 46 | 'message': 'invalid query' 47 | } 48 | }); 49 | appInstance.helloCall(); 50 | expect(appInstance.error).to.be.not.empty; 51 | done(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /template/server/plugins/HapiWebpackMiddleware/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Webpack = require('webpack'); 4 | const WebpackDevMiddleware = require('webpack-dev-middleware'); 5 | const WebpackHotMiddleware = require('webpack-hot-middleware'); 6 | 7 | const register = function (server, options, next) { 8 | const webpackCompiler = Webpack(options.config); 9 | const devMiddleware = WebpackDevMiddleware(webpackCompiler, options.devOptions); 10 | const hotMiddleware = WebpackHotMiddleware(webpackCompiler, options.hotOptions); 11 | 12 | server.ext({ 13 | type: 'onRequest', 14 | method: (request, h) => { 15 | return new Promise((resolve, reject) => { 16 | const rawRequest = request.raw.req; 17 | const rawResponse = request.raw.res; 18 | devMiddleware(rawRequest, rawResponse, (error) => { 19 | if (error) { 20 | return reject(error); 21 | } 22 | return resolve(h.continue); 23 | }); 24 | }); 25 | } 26 | }); 27 | 28 | server.ext({ 29 | type: 'onRequest', 30 | method: (request, h) => { 31 | return new Promise((resolve, reject) => { 32 | const rawRequest = request.raw.req; 33 | const rawResponse = request.raw.res; 34 | hotMiddleware(rawRequest, rawResponse, (error) => { 35 | if (error) { 36 | return reject(error); 37 | } 38 | return resolve(h.continue); 39 | }); 40 | }); 41 | } 42 | }); 43 | }; 44 | 45 | exports.plugin = { 46 | register: register, 47 | pkg: require('./package.json') 48 | }; 49 | -------------------------------------------------------------------------------- /template/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | browserSync = require('browser-sync'), 3 | path = require('path'), 4 | nodemon = require('gulp-nodemon'); 5 | 6 | // Run Nodemon and connect to BrowserSync 7 | gulp.task('nodemon', function (cb) { 8 | var called = false; 9 | return nodemon({ 10 | 11 | // Nodemon the dev server 12 | script: 'app.js', 13 | 14 | // Watch core server file(s) that require restart on change 15 | watch: ['app.js', 'server/**/*.*'] 16 | }) 17 | .on('start', function onStart() { 18 | 19 | // Ensure only one call 20 | if (!called) { 21 | called = true; 22 | return cb(); 23 | } 24 | }) 25 | .on('restart', function onRestart() { 26 | // Reload connected browsers after a slight delay 27 | console.log('Reload'); 28 | setTimeout(function () { 29 | browserSync.reload({ 30 | stream: false 31 | }); 32 | }, 1000); 33 | }); 34 | }); 35 | 36 | gulp.task('browser-sync', ['nodemon'], function () { 37 | 38 | // Config options: http://www.browsersync.io/docs/options/ 39 | browserSync.init({ 40 | 41 | // Watch the following files, inject changes or refresh 42 | files: ['public/assets/js/**/*.*', 'public/assets/css/**/*.*', 'public/assets/images/**/*.*'], 43 | 44 | // Proxy our Hapi app 45 | proxy: 'http://localhost:3000', 46 | // Use the following port for the proxied app to avoid clash 47 | port: 4000, 48 | reloadDelay: 500, 49 | injectChanges: true, 50 | open: false 51 | }); 52 | console.log('Initiate BrowserSync'); 53 | }); 54 | 55 | // BUILD 56 | 57 | gulp.task('default', ['browser-sync']); 58 | -------------------------------------------------------------------------------- /template/config/production.webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 6 | 7 | module.exports = { 8 | mode: 'production', 9 | entry: ['./client/main.js'], 10 | output: { 11 | path: path.resolve(__dirname, '../public/'), 12 | publicPath: '/', 13 | filename: 'build/build.js' 14 | }, 15 | resolve: { 16 | modules: [path.join(__dirname, '../node_modules')], 17 | extensions: ['.js', '.vue'], 18 | alias: { 19 | 'client': path.resolve(__dirname, '../client'), 20 | 'components': path.resolve(__dirname, '../clients/components'), 21 | 'vue$': 'vue/dist/vue.common.js' 22 | } 23 | }, 24 | module: { 25 | rules: [{ 26 | test: /\.vue$/, 27 | loader: 'vue-loader', 28 | options: { 29 | loaders: { 30 | 'css': ExtractTextPlugin.extract(['css-loader']), 31 | 'sass': ExtractTextPlugin.extract(['css-loader', 'sass-loader']) 32 | } 33 | } 34 | }, { 35 | test: /\.js$/, 36 | loader: 'babel-loader', 37 | exclude: /node_modules/ 38 | }, { 39 | test: /\.html$/, 40 | loader: 'vue-html-loader' 41 | }, { 42 | test: /\.(png|jpg|gif|svg)$/, 43 | loader: 'url-loader', 44 | query: { 45 | limit: 10000, 46 | name: '[name].[ext]?[hash]' 47 | } 48 | }] 49 | }, 50 | plugins: [ 51 | new webpack.optimize.UglifyJsPlugin({ 52 | sourceMap: true 53 | }), 54 | // https://github.com/ampedandwired/html-webpack-plugin 55 | new HtmlWebpackPlugin({ 56 | filename: path.resolve(__dirname, '../public/index.html'), 57 | template: path.resolve(__dirname, '../build/index_dev.html') 58 | }), 59 | new ExtractTextPlugin('build/style.css') 60 | ], 61 | devtool: 'source-map' 62 | } 63 | -------------------------------------------------------------------------------- /template/config/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const projectRoot = path.resolve(__dirname, '../') 4 | 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | {{#lint}} 7 | const EslintFriendlyFormatter = require('eslint-friendly-formatter') 8 | {{/lint}} 9 | 10 | module.exports = { 11 | mode: 'development', 12 | entry: ['webpack-hot-middleware/client', './client/main.js'], 13 | output: { 14 | path: path.resolve(__dirname, '../public/'), 15 | publicPath: '/', 16 | filename: 'build/build.js' 17 | }, 18 | resolve: { 19 | modules: [path.join(__dirname, '../node_modules'), 'node_modules'], 20 | extensions: ['.js', '.vue'], 21 | alias: { 22 | 'client': path.resolve(__dirname, '../client'), 23 | 'components': path.resolve(__dirname, '../clients/components'), 24 | 'vue$': 'vue/dist/vue.common.js' 25 | } 26 | }, 27 | module: { 28 | rules: [ 29 | {{#lint}} 30 | { 31 | enforce: "pre", 32 | test: /\.(js|vue)$/, 33 | loader: 'eslint-loader', 34 | include: projectRoot, 35 | exclude: /node_modules/, 36 | options:{ 37 | formatter: EslintFriendlyFormatter 38 | } 39 | }, 40 | {{/lint}} 41 | { 42 | test: /\.vue$/, 43 | loader: 'vue-loader', 44 | options: { 45 | loaders: { 46 | 'sass': 'vue-style-loader!css-loader!sass-loader' 47 | } 48 | } 49 | }, { 50 | test: /\.js$/, 51 | loader: 'babel-loader', 52 | exclude: /node_modules/ 53 | }, { 54 | test: /\.html$/, 55 | loader: 'vue-html-loader' 56 | }, { 57 | test: /\.(png|jpg|gif|svg)$/, 58 | loader: 'url-loader', 59 | query: { 60 | limit: 10000, 61 | name: '[name].[ext]?[hash]' 62 | } 63 | }] 64 | }, 65 | plugins: [ 66 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 67 | new webpack.HotModuleReplacementPlugin(), 68 | // https://github.com/ampedandwired/html-webpack-plugin 69 | new HtmlWebpackPlugin({ 70 | filename: path.resolve(__dirname, '../public/index.html'), 71 | template: path.resolve(__dirname, '../build/index_dev.html'), 72 | }) 73 | ], 74 | devtool: 'eval' 75 | } 76 | -------------------------------------------------------------------------------- /meta.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "prompts": { 3 | "name": { 4 | "type": "string", 5 | "required": true, 6 | "label": "Project name" 7 | }, 8 | "description": { 9 | "type": "string", 10 | "required": true, 11 | "label": "Project description", 12 | "default": "A Hapi Vue project" 13 | }, 14 | "author": { 15 | "type": "string", 16 | "label": "Author" 17 | }, 18 | "private": { 19 | "type": "boolean", 20 | "default": true 21 | }, 22 | "lint": { 23 | "type": "confirm", 24 | "message": "Include code linter (ESlint)?", 25 | "default": true 26 | }, 27 | "unit": { 28 | "type": "confirm", 29 | "message": "Include unit tests (Karma + Mocha + Sinon + Chai)?", 30 | "default": false 31 | }, 32 | "headlessBrowser": { 33 | "when": "unit", 34 | "type": "list", 35 | "message": "Pick a browser", 36 | "choices": [ 37 | { 38 | "name": "ChromeHeadless (Chrome >=59)", 39 | "value": "ChromeHeadless" 40 | }, 41 | { 42 | "name": "ChromiumHeadless (Chromium >=59)", 43 | "value": "ChromiumHeadless" 44 | }, 45 | { 46 | "name": "FirefoxHeadless (FF [Linux] >=55 [Win/Mac] >=56)", 47 | "value": "FirefoxHeadless" 48 | }, 49 | { 50 | "name": "PhantomJS", 51 | "value": "PhantomJS" 52 | } 53 | ] 54 | }, 55 | "unitApi": { 56 | "type": "confirm", 57 | "message": "Include dedicated, Hapi unit test utility (Lab + Chai)?", 58 | "default": false 59 | } 60 | }, 61 | "helpers": { 62 | "if_or": function (a, b, opts) { 63 | if (a || b) { 64 | return opts.fn(this) 65 | } 66 | return opts.inverse(this) 67 | }, 68 | "if_eq_or": function (option, a, b, opts) { 69 | if (option === a || option === b) { 70 | return opts.fn(this) 71 | } 72 | return opts.inverse(this) 73 | } 74 | }, 75 | "filters": { 76 | ".eslintrc.js": "lint", 77 | "test": "unit || unitApi", 78 | "test/unit/**/*": "unit", 79 | "test/api/**/*": "unitApi" 80 | }, 81 | "completeMessage": "To run dev version:\n\n cd {{destDirName}}\n npm install\n npm run dev\n\n" 82 | } 83 | -------------------------------------------------------------------------------- /template/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Hapi = require('hapi'); 4 | const Inert = require('inert'); 5 | const path = require('path'); 6 | 7 | const server = new Hapi.Server({ 8 | port: 3000 9 | }); 10 | 11 | async function initWebpackTools (middleware, config) { 12 | await server.register({ 13 | plugin: middleware, 14 | options: { 15 | config: config, 16 | devOptions: { 17 | logLevel: 'warn', 18 | publicPath: config.output.publicPath, 19 | stats: { 20 | colors: true 21 | } 22 | } 23 | } 24 | }); 25 | } 26 | 27 | // Register webpack HMR, fallback to development environment 28 | if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { 29 | const WebpackConfig = require('./config/webpack.config.js'); // Webpack config 30 | const HapiWebpackMiddleware = require('./server/plugins/HapiWebpackMiddleware'); 31 | initWebpackTools(HapiWebpackMiddleware, WebpackConfig); 32 | } 33 | 34 | server.register({ 35 | plugin: Inert 36 | }).then(() => { 37 | server.route({ 38 | method: 'GET', 39 | path: '/assets/{filepath*}', 40 | config: { 41 | auth: false, 42 | cache: { 43 | expiresIn: 24 * 60 * 60 * 1000, 44 | privacy: 'public' 45 | } 46 | }, 47 | handler: { 48 | directory: { 49 | path: path.join(__dirname, '/public/assets/'), 50 | listing: false, 51 | index: false 52 | } 53 | } 54 | }); 55 | 56 | server.route({ 57 | method: 'GET', 58 | path: '/build/{filepath*}', 59 | config: { 60 | auth: false, 61 | cache: { 62 | expiresIn: 24 * 60 * 60 * 1000, 63 | privacy: 'public' 64 | } 65 | }, 66 | handler: { 67 | directory: { 68 | path: path.join(__dirname, '/public/build/'), 69 | listing: false, 70 | index: false 71 | } 72 | } 73 | }); 74 | 75 | // Example api call 76 | server.route({ 77 | method: 'GET', 78 | path: '/api/call', 79 | handler: function (request, h) { 80 | return { 81 | message: 'Hello!' 82 | }; 83 | } 84 | }); 85 | 86 | server.route({ 87 | method: 'GET', 88 | path: '/{path*}', 89 | handler: { 90 | file: './public/index.html' 91 | } 92 | }); 93 | 94 | server.start(); 95 | }); 96 | 97 | server.events.on('start', (route) => { 98 | console.log('Server started'); 99 | }); 100 | 101 | module.exports = server; 102 | -------------------------------------------------------------------------------- /template/test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var path = require('path') 7 | var merge = require('webpack-merge') 8 | var baseConfig = require('../../config/webpack.config') 9 | var webpack = require('webpack') 10 | var projectRoot = path.resolve(__dirname, '../../') 11 | 12 | var webpackConfig = merge(baseConfig, { 13 | // use inline sourcemap for karma-sourcemap-loader 14 | devtool: '#inline-source-map' 15 | }) 16 | 17 | // no need for app entry during tests 18 | delete webpackConfig.entry 19 | 20 | webpackConfig.module.rules = webpackConfig.module.rules || [] 21 | webpackConfig.module.rules.unshift({ 22 | test: /\.js$/, 23 | include: path.resolve(projectRoot, 'client') 24 | }) 25 | 26 | webpackConfig.module.rules.some(function(loader, i) { 27 | if (loader.loader === 'babel') { 28 | loader.include = path.resolve(projectRoot, 'test/unit') 29 | return true 30 | } 31 | }) 32 | 33 | module.exports = function(config) { 34 | config.set({ 35 | // to run in additional browsers: 36 | // 1. install corresponding karma launcher 37 | // http://karma-runner.github.io/0.13/config/browsers.html 38 | // 2. add it to the `browsers` array below. 39 | {{#if_eq headlessBrowser "PhantomJS"}} 40 | browsers: ['PhantomJS'], 41 | {{/if_eq}} 42 | {{#if_eq headlessBrowser "ChromeHeadless"}} 43 | browsers: ['ChromeHeadless'], 44 | {{/if_eq}} 45 | {{#if_eq headlessBrowser "ChromiumHeadless"}} 46 | browsers: ['ChromiumHeadless'], 47 | {{/if_eq}} 48 | {{#if_eq headlessBrowser "FirefoxHeadless"}} 49 | browsers: ['FirefoxHeadless'], 50 | customLaunchers: { 51 | FirefoxHeadless: { 52 | base: 'Firefox', 53 | flags: [ '-headless' ] 54 | } 55 | }, 56 | {{/if_eq}} 57 | frameworks: ['mocha', 'sinon-stub-promise', 'sinon-chai'], 58 | reporters: ['spec', 'coverage'], 59 | files: ['./index.js'], 60 | preprocessors: { 61 | './index.js': ['webpack', 'sourcemap'] 62 | }, 63 | webpack: webpackConfig, 64 | webpackMiddleware: { 65 | noInfo: true 66 | }, 67 | coverageReporter: { 68 | dir: './coverage', 69 | reporters: [{ 70 | type: 'lcov', 71 | subdir: '.' 72 | }, { 73 | type: 'text-summary' 74 | }] 75 | }, 76 | client: { 77 | captureConsole: true 78 | }, 79 | singleRun: true 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hapi Vue 2 | 3 | > Development setup utilising Vue (Webpack HMR + Vue loader) and Hapi (Nodemon) running with Gulp and BrowserSync. It includes CSS extraction (build only), linting (ESlint) and unit testing (Karma, Mocha, Chai, Sinon, Lab). 4 | 5 | [Important!] Hapi Vue has been updated to Hapi 17, which introduced major, breaking changes - you can read more in the official [GitHub issue](https://github.com/hapijs/hapi/issues/3658). Version of Hapi Vue based on Hapi 16 can be found in the "archive" branch, [archive-hapi-16](https://github.com/Belar/hapi-vue/tree/archive-hapi-16). 6 | 7 | ## Mini-doc 8 | 9 | **/build/** - Contains files needed for build and hot development 10 | **/build/index_dev.html** - Template for index.html, it will be used by HMR when developing in memory and during production build 11 | **/client/** - Vue.js app source 12 | **/config/** - Configuration files 13 | **/public/** - Public folder served by Hapi 14 | **/server/** - Server side logic 15 | **/test/** - Contains test files 16 | **/app.js** - Production server 17 | **/gulpfile.js** - Gulp setup file 18 | 19 | ### Usage 20 | 21 | This is a project template for [vue-cli](https://github.com/vuejs/vue-cli). 22 | 23 | ``` bash 24 | $ npm install -g vue-cli 25 | $ vue init belar/hapi-vue 26 | $ cd 27 | ``` 28 | 29 | For tips and guides regarding integration of additional mechanisms (e.g vue-router), visit [Hapi Vue wiki](https://github.com/Belar/hapi-vue/wiki). 30 | 31 | NOTE: If you decide to run api (Hapi) unit tests using Mocha, remember about proper NODE_ENV (test), which with Lab is being set automatically. 32 | 33 | ## Dev Setup 34 | 35 | ``` bash 36 | # install dependencies 37 | npm install 38 | 39 | # run unit tests 40 | npm run unit 41 | 42 | # run api unit tests 43 | npm run unit-api 44 | 45 | # serve with hot reload at localhost:4000 (proxy from localhost:3000) 46 | npm run dev 47 | ``` 48 | 49 | ### Hapi Webpack Middleware 50 | 51 | Hapi Vue uses Hapi plugin system for integration of Webpack middlewares [web-dev-middleware](https://github.com/webpack/webpack-dev-middleware) and [webpack-hot-middleware](https://github.com/glenjamin/webpack-hot-middleware). 52 | 53 | Plugin accepts following options on registration: 54 | **config** - Webpack configuration file, `./config/webpack.config.js` by default. 55 | **devOptions** - Options for webpack-dev-middleware, as in the official documentation. 56 | **hotOptions** - Options for webpack-hot-middleware, as in the official documentation. 57 | ## Production Setup 58 | 59 | ``` bash 60 | # install dependencies 61 | npm install 62 | 63 | # build for production with minification 64 | npm run build 65 | 66 | # run application at localhost:3000 67 | node app.js 68 | ``` 69 | 70 | Credits: 71 | [Vue](https://vuejs.org/) 72 | [Hapi](http://hapijs.com/) 73 | [Gulp](https://gulpjs.com/) 74 | [BrowserSync](https://www.browsersync.io/) 75 | [Webpack](https://webpack.js.org/) 76 | -------------------------------------------------------------------------------- /template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{ name }}", 3 | "description": "{{ description }}", 4 | "author": "{{ author }}", 5 | {{#private}} 6 | "private": true, 7 | {{/private}} 8 | "scripts": { 9 | "dev": "gulp", 10 | {{#unit}} 11 | "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js", 12 | {{/unit}} 13 | {{#unitApi}} 14 | "unit-api": "lab test/api/spec/ -C -v -c -r console -o stdout -r html -o test/api/coverage/index.html --coverage-exclude .eslintrc.js", 15 | {{/unitApi}} 16 | "build": "webpack --progress --hide-modules --config config/production.webpack.config.js" 17 | }, 18 | "dependencies": { 19 | "axios": "^0.18.0", 20 | "babel-runtime": "^6.23.0", 21 | "hapi": "^17.1.1", 22 | "inert": "^5.0.1", 23 | "vue": "^2.3.4" 24 | }, 25 | "devDependencies": { 26 | "browser-sync": "^2.18.12", 27 | "cross-env": "^5.0.1", 28 | "gulp": "^3.9.1", 29 | "gulp-nodemon": "^2.2.1", 30 | "babel-core": "^6.25.0", 31 | "babel-loader": "^7.1.1", 32 | "babel-plugin-transform-runtime": "^6.23.0", 33 | "babel-preset-es2015": "^6.24.1", 34 | "babel-preset-stage-2": "^6.24.1", 35 | "html-webpack-plugin": "^3.0.6", 36 | "vue-hot-reload-api": "^2.1.0", 37 | "vue-html-loader": "^1.2.4", 38 | "vue-loader": "^14.2.1", 39 | "vue-template-compiler": "^2.3.4", 40 | "vue-style-loader": "^4.0.2", 41 | "css-loader": "^0.28.0", 42 | "file-loader": "^1.1.6", 43 | "url-loader": "^1.0.1", 44 | "node-sass": "^4.5.3", 45 | "sass-loader": "^6.0.6", 46 | {{#lint}} 47 | "eslint": "^4.1.1", 48 | "eslint-config-standard": "^11.0.0", 49 | "eslint-friendly-formatter": "^3.0.0", 50 | "eslint-loader": "^2.0.0", 51 | "eslint-plugin-html": "^4.0.1", 52 | "eslint-plugin-import": "^2.6.1", 53 | "eslint-plugin-node": "^6.0.1", 54 | "eslint-plugin-promise": "^3.5.0", 55 | "eslint-plugin-standard": "^3.0.1", 56 | {{/lint}} 57 | {{#if_or unit unitApi}} 58 | "chai": "^4.0.2", 59 | {{/if_or}} 60 | {{#unit}} 61 | "karma": "^2.0.0", 62 | "karma-coverage": "^1.1.1", 63 | "karma-mocha": "^1.3.0", 64 | "karma-sinon-chai": "^1.3.1", 65 | "karma-sinon-stub-promise": "^1.0.0", 66 | "karma-sourcemap-loader": "^0.3.7", 67 | "karma-spec-reporter": "0.0.32", 68 | "karma-webpack": "^2.0.3", 69 | "lolex": "^2.1.2", 70 | "mocha": "^5.0.4", 71 | "sinon": "^4.1.3", 72 | "sinon-chai": "^3.0.0", 73 | "sinon-stub-promise": "^4.0.0", 74 | "inject-loader": "^3.0.0", 75 | "babel-plugin-istanbul": "^4.1.4", 76 | {{#if_eq headlessBrowser "PhantomJS"}} 77 | "function-bind": "^1.1.0", 78 | "karma-phantomjs-launcher": "^1.0.4", 79 | "phantomjs-prebuilt": "^2.1.14", 80 | {{/if_eq}} 81 | {{#if_eq_or headlessBrowser "ChromeHeadless" "ChromiumHeadless"}} 82 | "karma-chrome-launcher": "^2.2.0", 83 | {{/if_eq_or}} 84 | {{#if_eq headlessBrowser "FirefoxHeadless"}} 85 | "karma-firefox-launcher": "^1.1.0", 86 | {{/if_eq}} 87 | {{/unit}} 88 | {{#unitApi}} 89 | "lab": "^15.1.2", 90 | {{/unitApi}} 91 | "webpack": "^4.1.1", 92 | "webpack-dev-middleware": "^3.0.1", 93 | "webpack-hot-middleware": "^2.18.0", 94 | "webpack-merge": "^4.1.0", 95 | "extract-text-webpack-plugin": "^3.0.0" 96 | } 97 | } 98 | --------------------------------------------------------------------------------