Skip to content

Latest commit

 

History

History
1556 lines (1200 loc) · 49 KB

server.litcoffee

File metadata and controls

1556 lines (1200 loc) · 49 KB

server.coffee

Static website creator for oj and the npm module Express templating engine and middleware

Include common modules

for m in ['path', 'fs', 'vm']
  global[m] = require m

Include dependencies

_ = require 'underscore'
coffee = require 'coffee-script'
url = require 'url'
mkdirp = require 'mkdirp'
csso = require 'csso'
uglifyjs = require 'uglify-js'

Static site creation runs in the context of the dom and jQuery

jsdom = (require 'jsdom').jsdom
global.jQuery = global.$ = require 'jquery'
global.document = jsdom "<html><head></head><body></body></html>"
global.window = document.createWindow()

Export Server Side OJ

Include the client library in this module

oj = require '../oj'

Indicate it is server side

oj.isClient = false

Export this module

module.exports = oj

Make sure jquery is hooked up

oj.$ = global.$

Store console codes for color logging

oj.codes =
  reset: '\u001b[0m'
  black: '\u001b[30m'
  red: '\u001b[31m'
  green: '\u001b[32m'
  yellow: '\u001b[33m'
  blue: '\u001b[34m'
  magenta: '\u001b[35m'
  cyan: '\u001b[36m'
  gray: '\u001b[37m'

Register require.extension for .oj and .ojc file types

if require.extensions

  coffee = require 'coffee-script'

  stripBOM = (c) -> if c.charCodeAt(0) == 0xFEFF then (c.slice 1) else c
  wrapJS = (code) ->
    "(function(){with(oj.sandbox){#{code}}}).call(this);"
  wrapCSMessage = (message, filepath) ->
    "#{oj.codes?.red}coffee-script error in #{filepath}: #{message}#{oj.codes?.reset}"
  wrapJSMessage = (message, filepath) ->
    "#{oj.codes?.red}javascript error in #{filepath}: #{message}#{oj.codes?.reset}"
  compileJS = (module, code, filepath) ->
    code = wrapJS code
    global.oj = oj
    module._compile code, filepath
    delete global.oj

Compile .oj files as javascript

  require.extensions['.oj'] = (module, filepath) ->
    # Read the file
    code = stripBOM fs.readFileSync filepath, 'utf8'
    try
      compileJS module, code, filepath
    catch eJS
      eJS.message = wrapJSMessage eJS.message, filepath
      throw eJS

Compile .ojc files as coffee-script

  require.extensions['.ojc'] = (module, filepath) ->

    code = stripBOM fs.readFileSync filepath, 'utf8'

    # Compile in coffee-script
    try
      code = coffee.compile code, bare: true
    catch eCoffee
      eCoffee.message = wrapCSMessage eCoffee.message, filepath
      throw eCoffee

    # Compile javascript
    try
      compileJS module, code, filepath

    catch eJS
      eJS.message = wrapJSMessage eJS.message, filepath
      throw eJS

Commands

oj.watch

Watch list of files or directories

oj.watch = (filesOrDirectories, options) ->
  options = _.extend {}, options,
    args: filesOrDirectories
    watch: true
    write: true
  oj.command options

oj.build

Build list of files or directories

oj.build = (filesOrDirectories, options) ->
  options = _.extend {}, options,
    args: filesOrDirectories
    watch: false
    write: true
  oj.command options

oj.command

Remember verbosity level

verbosity = null

oj.command options:

  • args: list of files or directories
  • debug: bool
  • watch: bool
  • recurse: bool
  • output: directory/path
  • modules: list of strings to include manually
  • verbose: level
  • html: Only output html
  • css: Only output css
  • js: Only output page js (no modules)
  • modules: Only output modules (no page rendering)

Define command

oj.command = (options = {}) ->

  verbosity = options.verbose || 1
  options.test ?= false
  options.write ?= not options.test     # Output to a file if not in test mode
  options.watch ?= false                # Watch for changes and recompile
  options.all ?= false                  # All defaults to off
  options.recurse ?= true               # Recurse in sub directories
  options.include ?= []                 # Include modules
  options.exclude ?= []                 # Exclude modules
  options.output ?= './public'

Resolve directory args to full path and append / to ensure path prefixes refer to directories

  options.output = (path.resolve process.cwd(), options.output) + '/'

Verify args exist

  throw new Error('oj: no args found') unless (_.isArray options.args) and options.args.length > 0

Convert args to full paths

  options.args = fullPaths options.args, process.cwd()

  for fullPath in options.args
    compilePath fullPath, _optionsForPath(fullPath, options),
      (err, results) ->

In test mode, log everything to the console instead of writing out files

        if options.test
          console.log results

  return

_optionsForPath

Some options like modulePath are dependent on what you are compiling For example if you compile ./website, should look in ./website/modules to find module bundle files, where as options like options.output are dependent on the current path.

_optionsForPath = (fullPath, options) ->

  options = _.clone options

Determine if user specified include options

  userSpecifiedInclude = options.modules? or options.html? or options.css? or options.js?

Resolve modulesDir and cssDir to the working directory if they were specified

  if options.modulesDir
    options.modulesDir = (path.resolve fullPath, options.modulesDir) + '/'

  if options.cssDir
    options.cssDir = (path.resolve fullPath, options.cssDir) + '/'

Include everything except modules if nothing specific has been included and if moduleDir exists (This case is the most common so detecting it helps usability)

  if not userSpecifiedInclude and fs.existsSync(options.modulesDir)
    options.modules = false
    options.js = options.css = options.html = true

Include everything if --all option is set or nothing is specified

  else if options.all or not userSpecifiedInclude
    options.modules = options.js = options.css = options.html = true

  options

compilePath

Compile any file or directory path

compilePath = (fullPath, options = {}, cb = ->) ->
  if isDirectory fullPath
    return compileDir fullPath, options, cb
  includeDir = path.dirname fullPath
  compileFile fullPath, includeDir, options, cb

compileDir

compileDir = (dirPath, options = {}, cb = ->) ->

Configure default modulesDir to be relative to directory being compiled if unspecified

  options = _.clone options
  if !options.modulesDir
    options.modulesDir = (path.resolve dirPath, (options.modulesDir ? './modules')) + '/'

Handle recursion and gather files to watch and compile

  lsWatch dirPath, options, (err, files, dirs) ->

Watch all directories if option is set

    if options.watch
      for d in dirs
        watchDir d, dirPath, options

Call cb when it has been called length times

    _cb = _.after files.length, cb

    for f in files

Compile and watch all pages

      if isOJPage f
        compileFile f, dirPath, options, _cb

Watch files that aren't pages

      else if options.watch
        watchFile f, dirPath, options
        _cb()
    return
  return

nodeModulePaths: Determine node_module paths from a given path

Implementation is from node.js' Module._nodeModulePaths This is not a public API so it seemed too horible to assume existance of

nodeModulePaths = (from) ->
  from = path.resolve from
  splitRe = (if process.platform == 'win32' then  /[\/\\]/ else /\//)
  joiner = (if process.platform == 'win32' then '\\' else '/')
  paths = []
  parts = from.split splitRe
  for tip in [(parts.length-1)..0]

Don't search in .../node_modules/node_modules

    if parts[tip] == 'node_modules'
      continue
    dir = parts.slice(0, tip + 1).concat('node_modules').join(joiner)
    paths.push(dir)
  paths

Recursively get final link path

resolveLink = (linkPath, out) ->
  try
    newPath = fs.readlinkSync linkPath
    return (resolveLink newPath, newPath)
  catch e
  out

Get module mapping from link dest to link source /some/path/linked-module -> /another/path/node_modules/linked-module

nodeModulesLinkMap = (fileDir) ->
  dirs = nodeModulePaths fileDir
  out = {}
  for dir in dirs
    try
      modules = fs.readdirSync dir
      for moduleName in modules
        modulePath = path.join dir, moduleName
        linkPath = resolveLink modulePath
        if linkPath?
          out[linkPath + '/'] = modulePath + '/'
    catch e
  out

# basenameForExtensions: Get basename for multiple extensions
basenameForExtensions = (p, arrayOfExt = []) ->
  out = path.basename p
  for ext in arrayOfExt
    out = path.basename out, ext
  out

compileFile

compileFile = (filePath, includeDir, options = {}, cb = ->) ->

  options = _.clone options

  # Time this method
  startTime = process.hrtime()

  # Clear underscore as modules might need it
  _clearRequireCacheRecord 'underscore'

  options.exclude ?= []

  throw new Error('oj: file not found') unless isFile filePath

  # Default some values
  isMinify = options.minify ? false

  includedModules = options.include or []
  includedModules = includedModules.concat ['oj', 'jquery']

  rootDir = options.root or path.dirname filePath
  fileDir = path.dirname filePath

Directory specifications win over options every time

  if options.modulesDir and _startsWith filePath, options.modulesDir
    options.modules = true
    options.css = false
    options.html = false
    options.js = false

  else if options.cssDir and _startsWith filePath, options.cssDir
    options.modules = false
    options.css = true
    options.html = false
    options.js = false

  throw new Error('oj: root is not a directory') unless isDirectory rootDir

  # Watch file if option is set
  if options.watch
    watchFile filePath, includeDir, options

  verbose 2, "compiling #{filePath}"

  # Cache of modules, files, and native modules
  cache = modules:{}, files:{}, native:{}

  # Determine global modules with soft linking
  moduleLinkMap = nodeModulesLinkMap fileDir

  # Hook require to intercept requires in oj files
  modules = {}
  moduleParents = {}  # map file name to parent list
  hookCache = {}
  hookOriginalCache = {}
  _hookRequire modules, moduleLinkMap, hookCache, hookOriginalCache

  # Save require cache to restore it later
  _saveRequireCache()

  # Remove excluded
  if _.isArray options.exclude
    for ex in options.exclude
      verbose 3, "excluding #{ex}"
    includedModules = _.difference includedModules, options.exclude

  # Catch messages thrown by requiring
  try

    # Require user defined modules
    for m in includedModules
      if isNodeModule m
        _buildNativeCacheFromModuleList cache.native, [m], isMinify
      else
        verbose 3, "including #{m}"
        require m

    # Require this file to start the process
    ojml = require filePath

  # Abort require with message on failure
  catch eRequire
    verbose 1, eRequire.message

    # Unwind ourselves from require before failing
    _restoreRequireCache()
    _unhookRequire modules, hookCache, hookOriginalCache
    return

  # Restore require cache
  _restoreRequireCache()
  _unhookRequire modules, hookCache, hookOriginalCache

  # Watch needs records of file dependencies
  _rememberModuleDependencies modules

Compile css if --css is set

  cssOption = !!options.css

Compile html only if --html is set

  htmlOption = !!options.html

Create compile options

  compileOptions =
    minify: isMinify
    html: htmlOption
    cssMap: cssOption
    css: cssOption
    dom: false
    data: options.data

  # Catch the messages thrown by compiling ojml
  try
    # Compile
    results = oj.compile compileOptions, ojml
  catch eCompile
    error "runtime error in #{filePath}: #{eCompile.message}"
    return

  # Build cache
  verbose 3, "caching #{filePath} (#{_length modules} files)"
  cache = _buildRequireCache modules, cache, isMinify

Calculate file locations for outputing

  # (assuming filePath is /input/dir/file.oj)
  # fileBaseName = file
  fileBaseName = basenameForExtensions filePath, ['.oj', '.ojc', 'ojlc']
  # outputDir = /output
  outputDir = options.output || process.cwd()
  # fileDir = /input/dir
  fileDir = path.dirname filePath
  # subDir = /dir
  subDir = path.relative includeDir, fileDir

Calculate the file extension. Default is .html

  extOut = '.html'

Use .css if only --css is specified

  if options.css and not (options.html or options.js or options.modules)
    extOut = '.css'

Use .js if css and html aren't specified

  else if (options.js or options.modules) and not (options.html or options.css)
    extOut = '.js'

Calculate file path to output to

  # fileOut = /output/dir/file.html
  fileOut = path.join outputDir, subDir, fileBaseName + extOut

Extend info to calculate output file locations and other meta data

  options.info = _.extend {},
    includeDir: includeDir
    # Profiling data
    startTime: startTime
    # Results of compile
    results: results
    # Module cache
    cache: cache
    filePath: filePath
    fileDir: fileDir
    fileBaseName: fileBaseName
    outputDir: outputDir
    subDir: subDir
    extOut: extOut
    fileOut: fileOut
    isMinify: isMinify

  _outputFile options, cb

  return

# _outputFile:
# Generic output file function that takes cached state and options and does the right thing
# including options for:
#   options.write:false output to callback instead of file
# ------------------------------------------------------------------------------
_outputFile = (options, cb) ->

  # Output html only as a .html
  if options.html and not (options.css or options.js or options.modules)
    _outputHtml options, cb
  # Output css as a .css file
  else if options.css and not (options.html or options.js or options.modules)
    _outputCss options, cb
  # Output js, modules or both as a .js file
  else if (options.js or options.modules) and not (options.html or options.css)
    _outputJs options, cb
  # Output some combination of html,css,js and modules in a .html file
  else
    _outputCombinedHtml options, cb

# outputUnifed
# ------------------------------------------------------------------------------

_outputCombinedHtml = (options, cb) ->
  info = options.info
  results = info.results
  filePath = info.filePath
  fileOut = info.fileOut
  html = results.html
  cssMap = results.cssMap
  cache = info.cache
  cacheLength = _length(cache.files) + _length(cache.modules) + _length(cache.native)
  verbose 3, "serializing #{filePath} (#{cacheLength} files)"
  scriptHtml = _requireCacheToHtml cache, filePath, options.minify ? false, options

  if !results.tags.html
    error "validation error #{filePath}: <html> tag is missing"
    return

  else if !results.tags.head
    error "validation error #{filePath}: <head> tag is missing"
    return

  else if !results.tags.body
    error "validation error #{filePath}: <body> tag is missing"
    return

  # TODO: Should we auto-insert doctype 5?
  # Insert script before </body> or before </html> or at the end
  scriptIndex = html.lastIndexOf '</body>'
  html = _insertAt html, scriptIndex, scriptHtml

  # Insert styles before </head> or after <html> or at the beginning
  styleIndex = html.indexOf '</head>'
  styleHTML = ''
  for plugin,mediaMap of results.cssMap
    styleHTML += oj._styleTagFromMediaObject plugin, mediaMap, options
  html = _insertAt html, styleIndex, styleHTML

  _outputDataToFileOrCallback html, options, cb

# _outputCss
# ------------------------------------------------------------------------------

_outputCss = (options, cb) ->
  _outputDataToFileOrCallback options.info.results.css, options, cb

# _outputHtml
# ------------------------------------------------------------------------------
_outputHtml = (options, cb) ->
  _outputDataToFileOrCallback options.info.results.html, options, cb

# _outputJs
# ------------------------------------------------------------------------------
_outputJs = (options, cb) ->
  info = options.info
  js = _requireCacheToJS info.cache, info.filePath, info.isMinify, options
  _outputDataToFileOrCallback js, options, cb

# _outputDataToFileOrCallback
# ------------------------------------------------------------------------------
# Output data to the fileOut location taking into account options.write flag
# If options.write is set the file is outputed otherwise it is sent only to the cb
_outputDataToFileOrCallback = (data, options, cb) ->

  info = options.info
  fileOut = info.fileOut

  filePath = info.filePath
  fileOut = info.fileOut

  # Create directory
  dirOut = path.dirname fileOut
  if mkdirp.sync dirOut
    verbose 3, "mkdir #{dirOut}"

  timeStamp = _timeStampFromStartTime info.startTime

  # Write file
  if options.write

    fs.writeFile fileOut, data, (err) ->
      if err
        error "file writing error #{filePath}: #{err}"
        return

      verbose 1, "compiled #{fileOut}#{timeStamp}", 'cyan'
      cb(null, data)
      return
  else
    verbose 1, "compiled #{fileOut}#{timeStamp}", 'cyan'
    cb(null, data)

_timeStampFromStartTime = (startTime) ->
  deltaTime = process.hrtime(startTime)
  timeStamp = " (#{deltaTime[0] + Math.round(10000*deltaTime[1]/1000000000)/10000} sec)"

# Keep track of which files are watched
watchCache = {}
isWatched = (fullPath) ->
  watchCache[fullPath]?
triggerWatched = (fullPath) ->
  watchCache[fullPath]?._events?.change?()
# Keep track of dependency tree of files
watchParents = {}

# watchFile
# ------------------------------------------------------------------------------

watchFile = (filePath, includeDir, options = {}) ->
  # Do nothing if this file is already watched
  return if isWatched filePath

  prevStats = null
  compileTimeout = null

  _watchErr = (e) ->
    if e.code is 'ENOENT'
      try
        _rewatch()
        _compile()
      catch e
        verbose 2, "unwatching missing file: #{filePath}", 'yellow'
        _unwatch()
    else throw e

  timeLast = new Date(2000)
  timeEpsilon = 2 # milliseconds

  _onWatch = ->
    try
      clearTimeout compileTimeout
      compileTimeout = wait 0.025, ->

        # Ignore recompiles within epsilon time
        timeNow = new Date()
        return if (timeNow - timeLast) / 1000 < timeEpsilon
        timeLast = timeNow

        # Files that aren't pages should trigger their parents
        if not isOJPage filePath
          parents = watchParents[filePath]
          if parents?
            for parent in parents
              triggerWatched parent

        # Files that are pages should recompile
        else
          fs.stat filePath, (err, stats) ->
            return _watchErr err if err
            verbose 2, "updating file #{filePath}", 'yellow'
            compileFile filePath, includeDir, options

    catch e
      verbose 1, 'unknown watch error on #{filePath}'
      _unwatch()
  try
    verbose 2, "watching file #{filePath}", 'yellow'
    watcher = fs.watch filePath, _onWatch
    watchCache[filePath] = watcher
  catch e
    _watchErr e

  _rewatch = ->
    verbose 3, "rewatch file #{filePath}", 'yellow'
    _unwatch()

    watchCache[filePath] = watcher = fs.watch filePath, _onWatch

  _unwatch = ->
    if isWatched filePath
      watchCache[filePath].close()
    watchCache[filePath] = null

watchDir

Watch a directory of files for new additions. This method does not recurse as it is called from methods that do (compileDir)

watchDir = (dir, includeDir, options) ->

  # Short circuit if already watching this directory
  return if isWatched dir

  # Throttle
  compileTimeout = null

  verbose 2, "watching directory #{dir}/", 'yellow'
  watcher = fs.watch dir, (err) ->
    verbose 2, "updating directory #{dir}/", 'yellow'

    # Unwatch missing directories
    if err and not isDirectory dir
      return unwatchDir dir

    # When the directory changes a file may have been added or removed
    # Watch all the directories and files that aren't currently being watched
    lsOJ dir, options, (err, files, dirs) ->
      for d in dirs
        if not isWatched d
          watchDir d
      for f in files
        if not isWatched f
          compileFile f, includeDir, options

  # Cache watch
  watchCache[dir] = watcher
  return

unwatchDir = (dir) ->
  verbose 2, "unwatching #{dir}/", 'yellow'
  if isWatched dir
    watchCache[dir].close()
  watchCache[dir] = null
  return

unwatchAll = ->
  verbose 2, "unwatching all files and directories", 'yellow'
  for k in _.keys watchCache
    if watchCache[k]?
      watchCache[k].close()
      watchCache[k] = null

# Cleanup watches on exit
process.on 'SIGINT', ->
  verbose 1, "\n"
  unwatchAll()
  verbose 1, "oj exited successfully.", 'cyan'
  process.exit()

Helpers

Output helpers

success = ->
  process.exit 0

tabs = (count) ->
  Array(count + 1).join('\t')

spaces = (count) ->
  Array(count + 1).join(' ')

# Print if verbose is set
verbose = (level, message, color = 'reset') ->
  if verbosity >= level
    console.log oj.codes[color] + "#{spaces(4 * (level-1))}#{message}" + oj.codes.reset

error = (message) ->
  red = oj.codes?.red ? ''
  reset = oj.codes?.reset ? ''
  console.error "#{red}#{message}#{reset}"
  return

File helpers

# isFile: Determine if path is to a file
isFile = (filePath) ->
  try
    (fs.statSync filePath).isFile()
  catch e
    false

isOJFile = (filePath) ->
  ext = path.extname filePath
  ext == '.oj' or ext == '.ojc'

# isOJPage: Determine if path is an oj page.
isOJPage = (filePath) ->
  ext = path.extname filePath
  base = path.basename filePath
  (isOJFile filePath) and base[0] != '_'and base.slice(0,2) != 'oj' and not isHiddenFile filePath

# isOJDir: Determine if path is an oj directory.
isOJDir = (dirPath, outputDir) ->
  base = path.basename dirPath
  base[0] != '_'and base[0] != '.' and base != 'node_modules'

# isWatchFile: Determine if file can be required and therefore is worth watching
isWatchFile = (filePath) ->
  ext = path.extname filePath
  (isOJFile filePath) or ext == '.js' or ext == '.coffee' or ext == '.json'

# isHiddenFile: Determine if file is hidden
isHiddenFile = (file) -> /^\.|~$/.test file

# isDirectory: Determine if path is to directory
isDirectory = (dirpath) ->
  try
    (fs.statSync dirpath).isDirectory()
  catch e
    false

# relativePathWithEscaping
# Example: '/User/name/folder1/file.oj' => '/file.oj'
relativePathWithEscaping = (fullPath, relativeTo) ->
  _escapeSingleQuotes '/' + path.relative relativeTo , fullPath

# fullPaths: Convert relative paths to full paths from origin dir
fullPaths = (relativePaths, dir) ->
  _.map relativePaths, (p) -> path.resolve dir, p

# commonPath: Given a list of full paths. Find the common root
commonPath = (paths, seperator = '/') ->
  if paths.length == 1
    return path.dirname paths[0]

  common = paths[0].split seperator
  ixCommon = common.length
  for p in paths
    parts = p.split seperator
    for part, ixPart in parts
      if common[ixPart] != part or ixPart > ixCommon
        break
    ixCommon = Math.min ixPart, ixCommon
  if ixCommon == 1 && paths[0][0] == seperator
    return seperator
  else if ixCommon == 0
    return null
  (common.slice 0, ixCommon).join seperator

# lsOJ
# Abstract if recursion happened and filters to only files / directories that don't start with _ and end in an oj filetype (.oj, .ojc, .ojlc)

lsOJ = (paths, options, cb) ->
  options ?= {}

  # Choose visible files with extension `.oj` and don't start with `oj` (plugins) or `_` (partials & templates)
  options = _.extend {},
    recurse: options.recurse
    filterFile: ((f) -> isOJPage f)
    filterDir:((d) -> isOJDir d)

  ls paths, options, (err, files, dirs) ->
    cb err, files, dirs
  return

lsWatch Look for all files I should consider watching. This includes: .js, .coffee, .oj, .ojc, with no limitations on hidden, underscore or oj prefixes.

lsWatch = (paths, options, cb) ->

Choose visible files with extension .oj and don't start with oj (plugins) or _ (partials & templates)

  lsOptions = _.extend {},
    recurse: options.recurse,
    filterFile: isWatchFile
    filterDir: isOJDir

  ls paths, lsOptions, (err, files, dirs) ->
    cb err, files, dirs
  return

ls: List directories and files from paths asynchronously options.filterFile: accept those files that return true options.filterDir: accept those directories that return true options.recurse: boolean to indicate recursion is desired

ls = (fullPath, options, cb, acc) ->

  # Optional options
  if _.isFunction options
    cb = options
    options = {}

  options ?= {}
  options.recurse ?= false
  options.filterFile ?= -> true # Keep everything by default
  options.filterDir ?= -> true # Keep everything by default
  options.recurseDepth ?= if options.recurse then Infinity else 1

  acc ?= {}
  acc.files ?= []
  acc.dirs ?= []
  acc.pending ?= 1

  breakIfDone = ->
    if acc.pending == 0
      files = _.uniq acc.files
      dirs = _.uniq acc.dirs
      cb null, files, dirs
    return

  fs.stat fullPath, (err, stat) ->
    return cb err if err

    # File found
    if stat.isFile() and options.filterFile(fullPath)
      acc.files.push fullPath
      return breakIfDone --acc.pending

    # Directory found
    else if stat.isDirectory() and options.filterDir(fullPath)
      acc.dirs.push fullPath

      fs.readdir fullPath, (errReadDir, paths) ->
        return cb errReadDir if errReadDir

        if !paths or paths.length == 0
          return breakIfDone --acc.pending

        # Directory has contents
        acc.pending += paths.length

        paths = fullPaths paths, fullPath

        # Recurse if we haven't hit max depth
        if options.recurseDepth > 0
          options_ = _.clone options
          options_.recurseDepth--
          for fullPath_ in paths
            ls fullPath_, options_, cb, acc

        return breakIfDone --acc.pending

    else
      return breakIfDone --acc.pending

  return

readFileSync = (filePath) ->
  fs.readFileSync filePath, 'utf8'

Timing helpers

wait = (seconds, fn) -> setTimeout fn, seconds*1000

String helpers

trimArgList = (v) ->
  _trim v.split(',')

# trim
_trim = (any) ->
  if _.isString any
    any.trim()
  else if _.isArray any
    out = _.map any, (v) -> v.trim()
    _.reject out, ((v) -> v == '' or v == null)
  else
    any

# startsWith
_startsWith = (strInput, strStart) ->
  throw new Error('startsWith: argument error') unless (_.isString strInput) and (_.isString strStart)
  strInput.length >= strStart.length and strInput.lastIndexOf(strStart, 0) == 0

_escapeSingleQuotes = (str) ->
  str.replace /'/g, "\\'"

_insertAt = (str, ix, substr) ->
  str.slice(0,ix) + substr + str.slice(ix)

_length = (any) ->
  any.length or _.keys(any).length

Code minification

oj._minifyJS = (js, options = {}) ->

  if options.filename
    verbose 4, "minified #{options.filename}"

  if options.minify
    uglifyjs js
  else
    js

oj._minifyCSS = (css, options = {}) ->
  if options.minify
    csso.justDoIt css, true # true means apply structural changes
  else
    css

Requiring

# Hooking into require inspired by [node-dev](http://github.com/fgnass/node-dev)
_hookRequire = (modules, moduleLinkMap, hookCache={}, hookOriginalCache={}) ->
  handlers = require.extensions
  for ext of handlers
    # Get or create the hook for the extension
    hook = hookCache[ext] or (hookCache[ext] = _createHook(ext, modules, moduleLinkMap, hookCache, hookOriginalCache))
    if handlers[ext] != hook
      # Save a reference to the original handler
      hookOriginalCache[ext] = handlers[ext]
      # and replace the handler by our hook
      handlers[ext] = hook
  return

# Hook into one extension
_createHook = (ext, modules, moduleLinkMap, hookCache, hookOriginalCache) ->
  (module, filename) ->

    # Unfortunately require resolves `filename` through soft links
    # For our module detection to work we need to unresolve these back
    # Use the moduleLinkMap to unresolve paths starting with `linkPath`
    # to start with `modulePath` instead.
    for linkPath, modulePath of moduleLinkMap
      # Prefix found then replace
      if 0 == filename.indexOf linkPath
        rest = filename.slice linkPath.length
        filename = modulePath + rest

    # Override compile to intercept file
    if !module.loaded
      _rememberModule modules, filename, null, module.parent.filename

      # if path.extension filename _.indexOf ['.coffee']
      moduleCompile = module._compile
      module._compile = (code) ->
        _rememberModule modules, filename, code, module.parent.filename
        moduleCompile.apply this, arguments
        return

    # Invoke the original handler
    hookOriginalCache[ext](module, filename)

    # Make sure the module did not hijack the handler
    _hookRequire modules, moduleLinkMap, hookCache, hookOriginalCache

_unhookRequire = (modules, hookCache, hookOriginalCache) ->
  handlers = require.extensions
  for ext of handlers
    if hookCache[ext] == handlers[ext]
      handlers[ext] = hookOriginalCache[ext]
  hookCache = null
  hookOriginalCache = null
  return

# Remember require references
_rememberModule = (modules, filename, code, parent) ->
  verbose 3, "requiring #{filename}" if code
  modules[filename] = _.defaults {code: code, parent:parent}, (modules[filename] or {})

_rememberModuleDependencies = (modules) ->
  for filename, module of modules
    watchParents[filename] ?= []
    watchParents[filename].push module.parent
    watchParents[filename] = _.unique watchParents[filename]

_nodeModulesSupported = oj:1, jquery:1, assert:1, console:1, crypto:1, events:1, freelist:1, path:1, punycode:1, querystring:1, string_decoder:1, tty:1, url:1, util:1
_nodeModuleUnsupported = child_process:1, domain:1, fs:1, net:1, os:1, vm:1, buffer:1
isNodeModule = (module) -> !!_nodeModulesSupported[module]
isUnsupportedNodeModule = (module) -> !!_nodeModuleUnsupported[module]
isAppModule = (module) -> (module.indexOf '/') == -1
isRelativeModule = (module) -> (module.indexOf '/') != -1

# _saveRequireCache
# Save a record of the cache so we can restore it later
_requireCache = null
_saveRequireCache = ->
  _requireCache = _.clone require.cache

# _restoreRequireCache
# Remove all recoreds in the cache that weren't there before
_restoreRequireCache = ->
  for k of require.cache
    delete require.cache[k] unless _requireCache[k]?

# Clear one record. This does not work in general because
# it doesn't recurse. It does work on stand alone modules
# like 'path' and 'underscore'
_clearRequireCacheRecord = (record) ->
  delete require.cache[require.resolve record]

# Parse code with
_getRequiresInSource = (code) ->
  r = new RegExp("require\\s*\\(?\\s*[\"']([^\"']+)", 'g');
  out = []
  while match = r.exec code
    out.push match[1]
  out

_first = (array, fn) ->
  for x in array
    y = fn x
    return y if y

# Build out the cache into the form:
#   cache.native = ['oj','jquery',...]
#   cache.modules = {<path/to/node_modules>: {<moduleName>:<path/to/main/file.js, ...}, ...}
#   cache.files = {<path/to/files.js>:<code>}

_buildRequireCache = (modules, cache, isMinify) ->
  for fileLocation, data of modules

    # Read code if it is missing
    if not data.code?
      throw new Error('data.code is missing')
      # data.code = stripBOM readFileSync fileLocation

    # Save code to _fileCache
    _buildFileCache cache.files, fileLocation, data.code, isMinify

    # Generate possible node_module paths given this file
    modulePrefixes = nodeModulePaths fileLocation

    pathComponents = _first modulePrefixes, (prefix) ->
      if _startsWith fileLocation, prefix + path.sep
        modulePath = (fileLocation.slice (prefix.length + 1)).split(path.sep)
        moduleName = modulePath[0]
        moduleMain = modulePath.slice(1).join(path.sep)
        modulesDir: prefix, moduleName:  moduleName, moduleMain: moduleMain, moduleParentPath: module.id

    # Save to cache.modules
    if pathComponents
      if not cache.modules[pathComponents.modulesDir]
        cache.modules[pathComponents.modulesDir] = {}
      # Example: /Users/evan/oj/node_modules: {underscore: 'underscore.js'}
      cache.modules[pathComponents.modulesDir][pathComponents.moduleName] = pathComponents.moduleMain

    # Build cache.native given source code in _fileCache
    _buildNativeCache cache.native, data.code, isMinify

    # Store complete
    verbose 4, "stored #{fileLocation}"

  # Remove client and server oj if they exist.
  # This is a special case of the way stuff is included.
  # To ourselves oj is local but to them oj is native.
  # Removing this cache record ensures only the native copy
  # is saved to the client.
  delete cache.files[require.resolve '../oj.js']

  # Separate files into three parts so each can be outputed separately:
  #   moduleFiles, pageFiles, nativeFiles
  cache.nativeFiles = cache.native
  cache.moduleFiles = {}
  cache.pageFiles = {}
  for filePath, code of cache.files
    if filePath.indexOf("/node_modules/") != -1
      cache.moduleFiles[filePath] = code
    else
      cache.pageFiles[filePath] = code
  return cache

# ###_buildFileCache: build file cache
_buildFileCache = (_filesCache, fileName, code, isMinify) ->
  # Minify code if necessary and cache it
  _filesCache[fileName] = oj._minifyJS code, {filename:fileName, minify: isMinify}

# build native module cache given moduleNameList
pass = 1

_buildNativeCache = (nativeCache, code, isMinify) ->
  # Get moduleName references from code
  moduleNameList = _getRequiresInSource code
  _buildNativeCacheFromModuleList nativeCache, moduleNameList, isMinify

_buildNativeCacheFromModuleList = (nativeCache, moduleNameList, isMinify) ->

  # Loop over modules and add them to native cache
  while moduleName = moduleNameList.shift()

    # Continue on already loaded native modules
    continue if nativeCache[moduleName]

    # OJ is built in
    if moduleName == 'oj'
      nativeCache.oj = _ojModuleCode isMinify

    # Do nothing if unsupported
    # Error checking happens earlier and missing modules at this stage are intentional
    else if isUnsupportedNodeModule moduleName
      pass

    else if isNodeModule moduleName
      # Get code
      codeModule = _nativeModuleCode moduleName, isMinify

      # Cache it
      nativeCache[moduleName] = codeModule

      # Concat all dependencies and continue on
      moduleNameList = moduleNameList.concat _getRequiresInSource codeModule

    else
      pass
  null

# ojModuleCode: Get code for oj
_ojModuleCode = (isMinify) ->
  code = readFileSync path.join __dirname, "../oj.js"
  oj._minifyJS code, filename:'oj', minify:isMinify

# nativeModuleCode: Get code for native module
_nativeModuleCode = (moduleName, isMinify) ->
  verbose 3, "found #{moduleName}"
  code = readFileSync path.join __dirname, "../modules/#{moduleName}.js"

Templating

Code generation

# ###_requireCacheToHtml
_requireCacheToHtml = (cache, filePath, isMinify, options) ->
  return """
    <script>
    #{_requireCacheToJS cache, filePath, isMinify, options}
    </script>
  """

# ###_requireCacheToJS
# Output html from cache and file
_requireCacheToJS = (cache, filePath, isMinify, options) ->
  newline = if isMinify then '' else '\n'

  # Example:
  #   filePath: /User/name/project/public/file.oj
  #   commonDir: /User/name/project
  #   clientDir: /public
  #   clientFile: /file

  # Calculate common path root between all cached files
  commonDir = commonPath _.keys cache.files

  # Determine directory for client using common path
  clientDir = '/' + path.relative commonDir, path.dirname filePath

  # Determine file for client given the above directory
  clientFile = path.join(clientDir, basenameForExtensions(filePath, ['.ojc', '.oj', '.coffee', '.js']))

  # Maps from moduleDir -> moduleName -> moduleMain such that
  # the file path is: moduleDir/moduleName/moduleMain
  _modulesToString = (moduleDir, nameToMain) ->
    # Use relative path to hide server directory info
    moduleDir = relativePathWithEscaping moduleDir, commonDir
    "M['#{moduleDir}'] = #{JSON.stringify nameToMain};\n"

  # Maps moduleName -> code for built in "native" modules
  _nativeModuleToString = (moduleName, code) ->
    # Use relative path to hide server directory info
    moduleName = _escapeSingleQuotes moduleName
    """F['#{moduleName}'] = (function(module,exports){(function(process,global,__dirname,__filename){#{newline}#{code}})(P,G,'/','#{moduleName}');});\n"""

  # Maps filePath -> code
  _fileToString = (filePath, code, prefixWithRequire) ->
    # Use relative and escaped path
    filePath = relativePathWithEscaping filePath, commonDir
    fileDir = path.dirname filePath
    fileName = path.basename filePath
    out = if prefixWithRequire then "require." else ""
    out += """F['#{filePath}'] = (function(module,exports){(function(require,process,global,__dirname,__filename){#{newline}#{code}})(require.RR('#{filePath}'),require.P,require.G,'#{fileDir}','#{fileName}');});#{newline}\n"""

  # Client side code to include modules
  _modules = ""
  # { '/Users/evan/oj/node_modules': { underscore: 'underscore.js' } }
  for moduleDir, nameToMain of cache.modules
    _modules += _modulesToString moduleDir, nameToMain

  # Client side code to include files from modules and from pages
  _moduleFiles = ""
  for filePath, code of cache.moduleFiles
    _moduleFiles += _fileToString filePath, code
    verbose 4, "serialized module file `#{filePath}`"

  _pageFiles = ""
  # Prefix require if modules aren't being output with this
  prefixWithRequire = !options.modules
  for filePath, code of cache.pageFiles
    _pageFiles += _fileToString filePath, code, prefixWithRequire
    verbose 4, "serialized page file `#{filePath}`"

  # Client side code to include native modules
  _nativeFiles = ""
  for moduleName, code of cache.native
    _nativeFiles += _nativeModuleToString moduleName, code
    verbose 4, "serialized native file '#{moduleName}'"

  # Load can be called with data or without
  _data = ''
  if not _.isEmpty options.data
    _data = ',' + JSON.stringify(options.data)
  _load = "oj.load('#{_escapeSingleQuotes clientFile}'#{_data});"

  # Browser side node has these abbreviations:
  #     G = Global
  #     P = Process
  #     RR = Require factory to generated require for a given path
  #     R = Require cache
  #     F = File cache
  #     M = Module cache

  # Client side function to run module and cache result
  _run = oj._minifyJS """
  function run(f){
      if(R[f] != null)
        return R[f];
      var eo = {},
        mo = {exports: eo};
      if(typeof F[f] != 'function')
        throw new Error("file not found (" + f + ")");
      F[f](mo,eo);
      return R[f] = mo.exports;
    }
""", minify:isMinify

  # Client side function to find module
  _find = oj._minifyJS """
  function find(m,f){
      var r, dir, dm, ext, ex, i, loc;
      if (F[m] && !m.match(/\\//))
        return m;

      if (!!m.match(/\\//)) {
          r = oj._pathResolve(f, oj._pathJoin(oj._pathDirname(f), m));
          ext = ['', '.oj', '.ojc', '.js', '.coffee', '.json'];
        for(i = 0; i < ext.length; i++){
          ex = ext[i];
          if((loc = r + ex) && F[loc])
            return loc;
          else if ((loc = oj._pathJoin(r, 'index' + ex)) && F[loc])
            return loc;
        }
      } else {
        if (typeof oj !== 'undefined') {
          dir = oj._pathDirname(f);
          while(true) {
            dm = oj._pathJoin(dir, 'node_modules');
            if(M[dm] && M[dm][m])
              return oj._pathJoin(dm, m, M[dm][m]);
            if(dir == '/')
              break;
            dir = oj._pathResolve(dir, '..');
          }
        }
      }
      throw new Error("module not found (" + m + ")");
    }
  """, minify: isMinify

  # Begin function
  js = """
    // Generated with oj v#{oj.version}
    ;(function(){
  """

Add modules if we want to include them

  if options.modules
    js += """
      var M = {}, F = {}, R = {}, P, G, RR;

      // Package modules
      #{_modules}
      #{_moduleFiles}
      // Native modules
      #{_nativeFiles}
      // Define node environment: process P, global G and require factory RR
      P = {cwd: function(){return '/'}}
      G = {process: P,Buffer: {}}
      RR = function(f){
        var o = function(m){return run(find(m, f))};
        o.P = P; o.G = G; o.F = F; o.M = M, o.RR = RR;
        return o;
        #{_run}
        #{_find}
      };

      // Define require and oj
      require = RR('/');
      oj = require('oj');\n
    """

Include page js if we want to include it

  if options.js
    js += """
      \n// Page files
      #{_pageFiles}
      #{_load}\n
    """
  # End function
  js += """
    }).call(this);
  """

Express View Engine

oj.__express = (path, options, cb) ->
  data = _.omit (_.clone options), 'settings', 'cache'
  _.defaults options,
    minify: options.minify
    write: false
    watch: false
    recurse: true
    modules: false
    html: true
    css: true
    js: true
    data: data
  compilePath path, options, cb
  return

Express Middleware

required options: publicDir: Public directory statically hosted by node modulesDirSrc: Directory to build module files from modulesDirDest: Directory to build module files to (should be public)

oj.middleware = (options) ->

Determine the url path relative the public directory

  urlModulesDir = '/' + path.relative(options.publicDir, options.modulesDirDest)

Output connect/express middleware

  (req,res,next) ->

Short circuit if this isn't a GET or HEAD request

    if 'GET' != req.method && 'HEAD' != req.method
      return next()

Get the path out of the url and figure out where to output the file

    urlPath = url.parse(req.url).pathname

The path starts with module dir and ends with .js

    if /\.js$/.test(urlPath) and _startsWith(urlPath, urlModulesDir)

      fileRelativePath = path.relative urlModulesDir, urlPath
      outputFilePath = path.join options.modulesDirDest, fileRelativePath
      outputFileDir = path.dirname outputFilePath

      baseFileName = path.basename urlPath, '.js'

      findFile options.modulesDirSrc, baseFileName, ['.oj', '.ojc'], (err, inputFilePath) ->

Found the file so lets get to compiling

        if inputFilePath

Compile only if the input file is newer then the output file or the force option is set

          if options.force
            _middlewareCompileModule()

          else
            fileIsOutdated outputFilePath, inputFilePath, (err, idOutdated) ->
              if idOutdated
                _middlewareCompileModule()
              else
                next()
              return

Compile using internal compilePath function that will generate the output to a directory

          _middlewareCompileModule = ->
            try
              compilePath inputFilePath,
                output: outputFileDir
                force: options.force ? false
                minify: options.minify ? true
                html:0, css:0, js:0, write:1, modules:1, ->
                  # File is compiled so let static middleware handle it
                  next()
            catch e
              next()

        else
          next()

The path doesn't match so just pass it on

    else
      next()

# Returns true if dest is older then source
fileIsOutdated = (fileDest, fileSource, cb) ->
  fs.stat fileSource, (errSource, statSource) ->
    return cb(errSource, null) if errSource
    fs.stat fileDest, (errDest, statDest) ->
      if !errDest || errDest.code == 'ENOENT'
        cb(null, !!errDest || statSource.mtime.getTime() > statDest.mtime.getTime())
      else
        cb(errDest, null)
  return

# Find file with given extensions in directory
findFile = (dir, baseFileName, extensions, cb) ->
    if !dir or !baseFileName or !extensions or !cb
      return cb('findFile: invalid input')

    # Fail if can't find file
    ext = extensions.shift()
    fileName = baseFileName + ext
    do (dir, fileName) ->
      fs.stat path.join(dir, fileName), (err, statInfo) ->
        if statInfo and statInfo.isFile()
          return cb null, path.join(dir, fileName)
        else if extensions.length > 0
          return findFile dir, baseFileName, extensions, cb
        else
          return cb null, null