Front-end system

Front-end abstraction: respond to client, render response, etc.

path = require 'path'

_ = require 'lodash'
cheerio = require 'cheerio'

pkgman = require 'pkgman'

config = require 'config'
{Config} = config

assets = null

exports.pkgmanRegister = (registrar) ->

Implements hook trussFrontendAssetsMiddleware

  registrar.registerHook 'trussFrontendAssetsMiddleware', ->

    label: 'Modules'
    middleware: [

      (req, assets, next) ->

Config script.

        clientConfig = new Config()

Use package list to build client package configuration.

        clientPackageConfig = new Config()
        for path in pkgman.packagesImplementing 'trussFrontendPackageConfig'
          clientPackageConfig.set(
            path.replace /\//g, ':'
            pkgman.invokePackage path, 'trussFrontendPackageConfig', req
          )

        clientConfig.set 'packageList', config.get 'packageList'
        clientConfig.set 'packageConfig', clientPackageConfig.toJSON()

Assign the config variable.

        assets.scripts.push
          type: 'inline'
          data: "window.__trussConfig = #{
            JSON.stringify clientConfig
          }"

Modules.

        if 'production' is config.get 'NODE_ENV'
          assets.scripts.push '/frontend/modules.min.js'
        else
          assets.scripts.push '/frontend/modules.js'

Serve livereload script if we aren't running in production mode.

        unless 'production' is config.get 'NODE_ENV'
          [hostname] = config.get('packageConfig:truss-http:hostname').split ':'
          assets.scripts.push "http://#{hostname}:35729/livereload.js"

        next()

    ]

Implements hook trussFrontendPackageTasks

  registrar.registerHook 'trussFrontendPackageTasks', (gruntConfig, grunt) ->

Watch rule.

    gruntConfig.configureTask 'watch', 'truss-frontend-truss-frontend', {
      files: ["packages/truss-frontend/client/require.coffee"]
      tasks: ['build:truss-frontend']
      options: livereload: true
    }

Build the require stub out-of-band, it shouldn't be included as a regular client module.

    gruntConfig.configureTask 'coffee', "truss-frontend-truss-frontend", files: [
      src: "packages/truss-frontend/client/require.coffee"
      dest: 'build/js/client/require.js'
    ]

    return 'newer:coffee:truss-frontend-truss-frontend'

Implements hook trussServerGruntConfig

  registrar.registerHook 'trussServerGruntConfig', (gruntConfig, grunt) ->

Cleaning rules.

    gruntConfig.configureTask 'clean', 'truss-frontend', ['build', 'frontend']

Build each package's frontend tasks.

    packageTasks = []
    for pkg in pkgman.packageList()

      tasks = if pkgman.packageImplements pkg, 'trussFrontendPackageTasks'
        pkgman.invokePackage(
          pkg, 'trussFrontendPackageTasks', gruntConfig, grunt
        )

      else

Watch rules.

        gruntConfig.configureTask 'watch', "truss-frontend-#{pkg}", {
          files: ["#{pkgman.packagePath pkg}/client/**/*.coffee"]
          tasks: ['build:truss-frontend']
          options: livereload: true
        }

Compilation rules.

        gruntConfig.configureTask 'coffee', "truss-frontend-#{pkg}", files: [
          src: "#{pkgman.packagePath pkg}/client/**/*.coffee"
          dest: 'build/js/client/raw'
          expand: true
          ext: '.js'
        ]

        "newer:coffee:truss-frontend-#{pkg}"

      gruntConfig.registerTask 'truss-frontend-packages', tasks

Browserify modules.

    gruntConfig.configureTask 'browserify', 'truss-node-path', {
      src: ['node_modules/path-browserify/index.js']
      dest: 'build/js/client/raw/path.js'
      options: browserifyOptions: standalone: 'path'
    }
    gruntConfig.registerTask 'truss-frontend-browserify', [
      'newer:browserify:truss-node-path'
    ]

Wrap all modules with the require function.

    gruntConfig.configureTask(
      'wrap', 'truss-frontend'

      files: [
        src: [
          'build/js/client/raw/**/*.js'
        ]
        dest: 'build/js/client/modules-raw.js'
      ]
      options:
        indent: '  '
        wrapper: (filepath) ->

Slice past the number of slashes in build/js/client/raw.

          modulePath = filepath.split('/').slice(4).join '/'

Little acrobatics to handle root-level paths.

          dirname = path.dirname modulePath
          if '.' is dirname = path.dirname modulePath
            dirname = ''
          else
            dirname += '/'
          basename = path.basename modulePath, path.extname modulePath

          [
            """
_requires['#{
  dirname
}#{
  basename
}'] = function(module, exports, require, __dirname, __filename) {


"""
            """

};


"""
          ]

    )

Concatenate the require stub to the bottom of the modules.

    gruntConfig.configureTask 'concat', 'truss-frontend', files: [
      src: [
        'build/js/client/modules-raw.js'
        'build/js/client/require.js'
      ]
      dest: 'build/js/client/modules-raw-with-require.js'
    ]

Wrap the whole thing in an IIFE and define the _requires object to hold all module implementations.

    gruntConfig.configureTask(
      'wrap', 'truss-frontend-modules'

      files: [
        src: [
          'build/js/client/modules-raw-with-require.js'
        ]
        dest: 'frontend/modules.js'
      ]
      options:
        indent: '  '
        wrapper: [
          '(function() {\n\n  var _requires = {};\n\n\n'
          '\n\n})();\n\n'
        ]

    )

Minimize the module sources.

    gruntConfig.configureTask 'uglify', 'truss-frontend', files: [
      src: [
        'frontend/modules.js'
      ]
      dest: 'frontend/modules.min.js'
    ]

    gruntConfig.registerTask 'production', ['uglify:truss-frontend']

    gruntConfig.registerTask 'build:truss-frontend', [
      'truss-frontend-packages'
      'truss-frontend-browserify'
      'newer:wrap:truss-frontend'
      'newer:concat:truss-frontend'
      'newer:wrap:truss-frontend-modules'
    ]

    gruntConfig.registerTask 'build', ['build:truss-frontend']

    gruntConfig.loadNpmTasks [
      'grunt-browserify'
      'grunt-contrib-clean'
      'grunt-contrib-coffee'
      'grunt-contrib-concat'
      'grunt-contrib-uglify'
      'grunt-contrib-watch'
      'grunt-newer'
      'grunt-wrap'
    ]

Implements hook trussHttpServerRoutes

  registrar.registerHook 'config', 'trussHttpServerRoutes', -> [
    path: '/frontend/modules.js'
    receiver: (req, res, next) -> require('fs').createReadStream(
      "#{config.get 'path'}/frontend/modules.js"
    ).pipe res
  ]

Implements hook trussHttpServerRequestMiddleware

  registrar.registerHook 'trussHttpServerRequestMiddleware', ->

    debug = require('debug') 'truss-silly:assets:middleware'

    middleware = require 'middleware'

Invoke hook trussFrontendAssetsMiddleware

    debug '- Loading asset middleware...'

    assetsMiddleware = middleware.fromConfig 'truss-frontend:assetsMiddleware'

    debug '- Asset middleware loaded.'

    label: 'Serve frontend'

    middleware: [

      (req, res, next) ->

        $ = res.$ = cheerio.load '''
<!doctype html><html><head></head><body></body></html>
'''

Add mobile-first tags.

        head = $('head')
        head.append $('<meta>').attr 'charset', 'utf-8'
        head.append $('<meta>').attr(
          name: 'viewport'
          content: 'width=device-width, initial-scale=1.0'
        )

Gather assets.

        body = $('body')
        assets = scripts: [], styleSheets: []
        assetsMiddleware.dispatch req, assets, (error) ->
          return next error if error?

Inject scripts.

          for script in assets.scripts
            if _.isString script
              script = type: 'remote', data: script

            $script = $('<script>').attr type: 'text/javascript'

            body.append switch script.type

              when 'remote'
                $script.attr src: script.data

              when 'inline'
                $script.html script.data

Inject CSS.

          for styleSheet in assets.styleSheets
            if _.isString styleSheet
              styleSheet = type: 'remote', data: styleSheet

            body.append switch styleSheet.type

              when 'remote'
                $('<link>').attr rel: 'stylesheet', href: styleSheet.data

              when 'inline'
                $('<style>').attr(type: 'text/css').html styleSheet.data

Build the HTML and serve it.

          res.delivery = res.$.html()
          next()

    ]

Implements hook trussServerPackageConfig

  registrar.registerHook 'trussServerPackageConfig', ->

    assetsMiddleware: [
      'truss-frontend'
    ]