UNPKG

oj

Version:

A unified templating language for the people. Thirsty people.

1,545 lines (1,196 loc) 50.2 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, pa