Skip to content

Instantly share code, notes, and snippets.

@ggoodman
Created August 29, 2014 17:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ggoodman/149751343c22892b9871 to your computer and use it in GitHub Desktop.
Save ggoodman/149751343c22892b9871 to your computer and use it in GitHub Desktop.
Build system and folder structure of Plunker.NEXT

The build system and directory structure for Plunker.NEXT

Tools used:

  • Angular.js
  • ui-router
  • Webpack
  • Less
  • Bower
  • npm

Goals:

  1. Filenames are not too ambiguous. Ie: avoid index.js and favour searchBox.js to ease IDE usage.
  2. Files are organized by their nature.
  3. Rapid prototyping.

Setup

All client-side source files live in the ./src folder. Webpack is used to put everything together in the plunker.js file of the ./static directory (could easily be called dist).

Vendor client-side libraries are put in the bower_components folder by bower (which is committed to git).

Vendor npm modules are used when available and well-adapted for client-side usage. Examples include semver and lodash.

Webpack is started in the watch mode at server startup. The server will only listen() upon successful build of the assets. Rebuilds (which are incremental thanks to Webpack) are only logged and do not affect the server.

Important: All Angular.js modules have their module definition exported in a CommonJS way. For vendor angular stuff, I built a custom loader to do this for modules outside my control (see below).

webpack.config.js

var NgAnnotatePlugin = require("ng-annotate-webpack-plugin");
var Path = require("path");
var Webpack = require("webpack");

module.exports = function (config) {
  return {
    cache: true,
    entry: {
      plunker: [__dirname + "/src/apps/plunker.js"],
    },
    output: {
      path: Path.join(__dirname, "static", "build"),
      filename: "[name].js",
      publicPath: "/static/",
    },
    module: {
      loaders: [
        { test: /[\/\\]ace\.js$/, loader: "exports-loader?window.ace" },
        { test: /[\/\\]angular\.js$/, loader: "exports-loader?window.angular" },
        { test: /[\/\\]angular-cookie\.js$/, loader: "ng-loader?ipCookie" },
        { test: /[\/\\]angular-deckgrid\.js$/, loader: "ng-loader?akoenig.deckgrid" },
        { test: /[\/\\]angular-ui-router\.js$/, loader: "ng-loader?ui.router" },
        { test: /[\/\\]timeAgo\.js$/, loader: "ng-loader?yaru22.angular-timeago" },
        { test: /[\/\\]ui-bootstrap-tpls\.js$/, loader: "ng-loader?ui.bootstrap" },
        { test: /\.css$/,   loader: "style-loader!css-loader" },
        { test: /\.less$/,  loader: "style-loader!css-loader!less-loader" },
        { test: /\.woff$/,  loader: "url-loader?limit=10000&mimetype=application/font-woff" },
        { test: /\.ttf$/,   loader: "file-loader" },
        { test: /\.eot$/,   loader: "file-loader" },
        { test: /\.svg$/,   loader: "file-loader" },
        { test: /\.html$/,  loader: "raw-loader" },
        { test: /\.json$/,  loader: "json-loader" },
      ],
      noParse: [
        /[\/\\]angular\.js$/,
        /[\/\\]ace\.js$/,
        /[\/\\]angular-cookie\.js$/,
        /[\/\\]angular-deckgrid\.js$/,
        /[\/\\]angular-ui-router\.js$/,
        /[\/\\]timeAgo\.js$/,
        /[\/\\]ui-bootstrap-tpls\.js$/,
      ]
    },
    plugins: [
      new PlunkerModuleReplacementPlugin(),
      new Webpack.DefinePlugin({
        CONFIG: JSON.stringify(config)
      }),
      // new Webpack.optimize.DedupePlugin(),
      // new NgAnnotatePlugin(),
      // new Webpack.optimize.UglifyJsPlugin({
      //   mangle: false,
      //   compress: false,
      // }),
    ],
    resolve: {
      modulesDirectories: ["node_modules", "bower_components", "src"],
      root: __dirname,
      alias: {
        'ace': "ace-builds/src-noconflict/ace.js",
        'angular': "angular/angular.js",
        'angular-cookie': "angular-cookie/angular-cookie.js",
        'angular-deckgrid': "angular-deckgrid/angular-deckgrid.js",
        'angular-timeago': "angular-timeago/src/timeAgo.js",
        'angular-ui-router': "angular-ui-router/release/angular-ui-router.js",
        'angular-ui-bootstrap': "angular-bootstrap/ui-bootstrap-tpls.js",
      },
    },
  };
};



function PlunkerModuleReplacementPlugin () {
  this.resourceRegExp = /^plunker(?:\.(\w+))+$/;
}
PlunkerModuleReplacementPlugin.prototype.apply = function (compiler) {
  var resourceRegExp = this.resourceRegExp;
  compiler.plugin("normal-module-factory", function (nmf) {
    nmf.plugin("before-resolve", function (result, callback) {
      if (!result) return callback();
      
      if (resourceRegExp.test(result.request)) {
        var parts = result.request.split(".").slice(1);
        
        result.request = parts.join("/");
      }

      return callback(null, result);
    });
  });
};

What's going on there?!

I've added a couple custom bits to this webpack config.

  1. PlunkerModuleReplacementPlugin
  2. ng-loader

PlunkerModuleReplacementPlugin is used to map semantic module names to their filesystem paths. It will only affect requires with package names having the plunker. prefix. For example: require('plunker.states.layout') will be re-mapped to require('states/layout').

ng-loader is a webpack loader that will basically allow me to inject some code at the end of a vendor module so that I can export the module as per CommonJS. In the loaders section of the webpack.config.js file, note the ng-loader?ui-router; what that does is that it will inject module.exports = window.angular.module('ui-router'); at the end of the file.

Why go through that hassle?!

I don't like having two mechanisms for locating and managing dependencies: 1) physical location of package (and filesystem name); 2) angular module name. This system lets me keep things very clean and self-contained:

require("./layout/layout.less");


var Angular = require("angular");


module.exports =
Angular.module("plunker.states.layout", [
  require("plunker.components.searchBox").name,
  require("plunker.components.userMenu").name,
  
  require("plunker.controllers.search").name,
])

.config(["$stateProvider", function ($stateProvider) {
  $stateProvider.state("layout", {
    virtual: true,
    views: {
      '': {
        template: require("./layout/layout.html"),
      },
    },
  });
}])

;

Folder structure:

plunker-web-rewrite/src
___ apps
___ ___ plunker.js
___ common
___ ___ colors.less
___ ___ flexbox.less
___ ___ material.less
___ components
___ ___ markdown
___ ___ ___ markdown.less
___ ___ markdown.js
___ ___ plunkCard
___ ___ ___ plunkCard.html
___ ___ ___ plunkCard.less
___ ___ plunkCard.js
___ ___ searchBox
___ ___ ___ searchBox.html
___ ___ ___ searchBox.less
___ ___ searchBox.js
___ ___ userMenu
___ ___ ___ userMenu.html
___ ___ userMenu.js
___ controllers
___ ___ search.js
___ services
___ ___ api.js
___ ___ visitor.js
___ states
    ___ layout
    ___ ___ explore
    ___ ___ ___ explore.html
    ___ ___ ___ explore.less
    ___ ___ explore.js
    ___ ___ landing
    ___ ___ ___ landing.html
    ___ ___ ___ landing.less
    ___ ___ ___ tb-left.html
    ___ ___ landing.js
    ___ ___ layout.html
    ___ ___ layout.less
    ___ ___ plunk
    ___ ___ ___ plunk.html
    ___ ___ ___ plunk.less
    ___ ___ ___ runner.html
    ___ ___ ___ tb-left.html
    ___ ___ plunk.js
    ___ layout.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment