Skip to content

niallobrien/hackathon-starter-plus

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hackathon Starter+ Hackathon Starter+

This is a project which builds off the foundations of a node.js boilerplate called Hackathon Starter.

Hackathon Starter focuses on providing a simple and easy to use node.js boilerplate for you to hit the ground running. However, if you want to take your app to production, there are a few changes you will want to make to the boilerplate to take it beyond a hackathon project.
Also, since Hackathon Starter is intended for everyone (including beginners), there are certain things that will never be included out of the box, such as Socket.io (websockets) support.

Hackathon Starter+ aims to provide a post hackathon guide so that you can take your projects to the next level.

Table of Contents

Features

  • Asset Pipeline using Laravel Mix, Webpack with styles and scripts project skeleton structure
  • Websocket Support via Socket.io

Prerequisites

You need to have an existing Hackathon Starter project in place (existing or new).
See Getting Started within the Hackathon Starter documentation.

Getting Started

If you intend to use any of the files included with Hackathon Starter+, I highly recommend that you don't clone this repo into your current Hackathon Starter project.
Instead, download the .zip file of this repo or just manually adjust/create the files where needed in your project and copy & paste over whatever pieces you want.

Remember: Make sure that you copy/rename .env.example to .env and populate the environment variables with your own keys/secrets.

Asset Pipeline

We're going to continue to use Bootstrap 4, Font-Awesome etc. however, what we want is to be able to build a production-ready version of our app that utilises some best practices, such as:

  • Development vs Production: Handle development assets differently from production assets.
  • Filename Fingerprinting: Each client-side file will get have a unique string of characters added to the filename. Whenever the contents of a file changes, the fingerprinted filename for production will also change. This will provide us with a good cache-busting solution.
  • Sass File Location: Sass files are moved out of the public directory so that no one can access our source Sass files.
  • Client-side JavaScript: Provide a good initial structure for your custom JavaScript code using the module revealing pattern.
  • Code Splitting: separate our vendor (3rd party) JavaScript dependencies from our own custom JavaScript code.
  • Pre-built Assets: All Sass and JavaScript files are processed by Webpack in advance and not at runtime (as is the case with Sass middleware using the default Hackathon Starter setup).

What To Remove

Note: Line locations may vary based on what you have added/removed.

npm uninstall jquery node-sass node-sass-middleware --save

Delete these folders:
public/css
public/js
public/webfonts

Open app.js at the top level of your project and remove line 20 and the middleware shown below from lines 66-69

app.use(sass({
  src: path.join(__dirname, 'public'),
  dest: path.join(__dirname, 'public')
}));

What To Add

Note: Line locations may vary based on what you have added/removed.

npm install cross-env bootstrap glob-all jquery laravel-mix popper.js purgecss-webpack-plugin tooltip.js simple-pjax @fortawesome/fontawesome @ladjs/bootstrap-social --no-optional --save-dev

Copy the assets folder from this repo to your project.
Copy the .gitignore file from this repo to your project. Copy the .webpack.mix.js file from this repo to your project.

We now need to modify our app to handle development and production assets as the file names will be different.
In app.js (where we create our Express server), in between lines 64 and 65 (after app.set('view engine', 'pug');), add the following:

// Middleware for Jade/Pug custom filter for use with Laravel Mix
app.use((req, res, next) => {
  app.locals.filters = {
    'mix': (text, options) => {
      if (!text) return
      text = text.replace(/["']/g, '')

      const manifest = require(__dirname + '/public/mix-manifest.json')
      if (options.css) return `<link rel="stylesheet" href="${manifest[text]}">`
      if (options.js) return `<script type="text/javascript" src="${manifest[text]}"></script>`
    }
  }
  next()
})

In views/layout.pug replace line 11 with

:mix(css) '/assets/styles/app.css'

At line 25 add

:mix(js) '/assets/scripts/manifest.js'
:mix(js) '/assets/scripts/vendor.js'
:mix(js) '/assets/scripts/app.js'

Now that our app is updated, we just need to update how we run these new tasks. Open the package.json file at the top level of the project and replace the scripts section with the below:

"scripts": {
    "start": "node app.js",
    "dev": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "hot": "cross-env NODE_ENV=development webpack-dev-server --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
    "production": "NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "prod": "npm run production",
    "test": "nyc mocha --timeout=3000 --exit",
    "lint": "eslint **/*.js"
  },
  • simple-pjax is included by default to provide an experience that feels more like a single page app. You can read more about it here.
  • If you are not going to use Websockets with Socket.io, feel free to remove references to socket.io from assets/scripts/app.js.
  • Perhaps consider renaming app.js at the top level of the project to server.js so that you don't get confused between our server file and client-side JavaScript file with the same name. Also remember to update the start task within the scripts block in package.json if you do rename the file. Eg. "start": "node server.js",

If you are unsure of these changes, please see the example files within this repo for reference.

Development

When developing, it's best to have two terminal windows open. One window will be running your asset pipeline, watching for changes and recompiling when necessary. The other window will have the Express server running (make sure you've already started MongoDB). Also note, it's worthwhile to use Nodemon to start your server as it will automatically detect changes and restart your server for you.
If you opt to use Nodemon, replace "start": "node app.js", with nodemon app.js.

In the first terminal window, run npm start.
In the second terminal run npm run watch. This will watch our Sass files and our client-side JavaScript for changes and automatically recompile when needed.

Production

To compile your assets for production, run npm run prod and production ready versions of your assets will be output in the public directory. Be sure to deploy these files.

Websocket Support

Note: Line locations may vary based on what you have added/removed.

What To Add

npm install socket.io --save

There's a few pieces to be added to app.js to add websocket support.

Find this at line 46

const app = express();

Add this after line 46

/**
 * Create Express & Socket.io servers.
 */
const socketIoPort = 3001
const app = express()
const server = require('http').Server(app)
const io = require('socket.io')(socketIoPort)

Find this at line 65

app.use(expressStatusMonitor());

Replace with this:

app.use(expressStatusMonitor({websocket: io, port: socketIoPort}))

Find this at line 93

app.use((req, res, next) => {
  res.locals.user = req.user;
  next();
});

Replace with this:

app.use((req, res, next) => {
  // Construct url to socket.io (port 3001). Slice port number where not needed.
  res.locals.hostname = process.env.BASE_URL.slice(0, -5) || req.protocol + '://' + req.hostname
  res.locals.fullHostname = process.env.BASE_URL || req.protocol + '://' + req.hostname + ':' + req.app.settings.port
  res.locals.user = req.user
  next()
})

At line 227 add

/**
 * Socket.io.
 */
io.on('connection', (socket) => {
  socket.emit('greet', {hello: 'Hey there browser!'})
  socket.on('respond', (data) => {
    console.log(data)
  })
  socket.on('disconnect', () => {
    console.log('Socket disconnected')
  })
})

The following piece of jQuery will listen for a connection to our Socket.io server. Once established it will then listen for the greet event and emit a respond event when a greet event is received.

$(document).ready(function () {
  // init socket.io
  const socket = io.connect(window.location.hostname + ':3001')
  socket.on('greet', (data) => {
    console.log(data)
    socket.emit('respond', { message: 'Hey there, server!' })
  })
})

To summarise: When a websocket connection is established, our server emits a greet event over websockets with a message saying "Hey there browser!" and when our browser receives this event, it logs what was sent from the server to its (the browser's) console.
We then emit an event from the client called respond and when the server receives this event, it logs the message (on the server) that was sent, in this case "Hey there, server!"

Contributing

If something is unclear, confusing, or needs to be refactored, please let me know.
Pull requests are always welcome, but due to the opinionated nature of this project, I cannot accept every pull request. Please open an issue before submitting a pull request.
This project uses Airbnb JavaScript Style Guide with a few minor exceptions.
If you are submitting a pull request that involves Pug templates, please make sure you are using spaces, not tabs.

License

The MIT License (MIT)
Copyright (c) 2018 Niall O'Brien
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.