Permalink
Fetching contributors…
Cannot retrieve contributors at this time
307 lines (206 sloc) 8.35 KB

Require

A Node.js compatible require implementation for pure client side apps.

Each file is a module. Modules are responsible for exporting an object. Unlike traditional client side JavaScript, Ruby, or other common languages the module is not responsible for naming its product in the context of the requirer. This maintains encapsulation because it is impossible from within a module to know what external name would be correct to prevent errors of composition in all possible uses.

Uses

From a module require another module in the same package.

require "./soup"

Require a module in the parent directory

require "../nuts"

Require a module from the root directory in the same package.

NOTE: This could behave slightly differently under Node.js if your package does not have it's own isolated filesystem.

require "/silence"

From a module within a package, require a dependent package.

require "console"

The dependency could be delcared in pixie.cson as follows:

dependencies:
  console: "http://strd6.github.io/console/v1.2.2.json"

You can require a package directly from its JSON representation as well.

$.getJSON(packageURL)
.then (pkg) ->
  require pkg

Implementation

File separator is '/'

fileSeparator = '/'

In the browser global is self.

global = self

Default entry point

defaultEntryPoint = "main"

A sentinal against circular requires.

circularGuard = {}

A top-level module so that all other modules won't have to be orphans.

rootModule =
  path: ""

Require a module given a path within a package. Each file is its own separate module. An application is composed of packages.

loadPath = (parentModule, pkg, path) ->
  if startsWith(path, '/')
    localPath = []
  else
    localPath = parentModule.path.split(fileSeparator)

  normalizedPath = normalizePath(path, localPath)

  cache = cacheFor(pkg)

  if module = cache[normalizedPath]
    if module is circularGuard
      throw "Circular dependency detected when requiring #{normalizedPath}"
  else
    cache[normalizedPath] = circularGuard

    try
      cache[normalizedPath] = module = loadModule(pkg, normalizedPath)
    finally
      delete cache[normalizedPath] if cache[normalizedPath] is circularGuard

  return module.exports

To normalize the path we convert local paths to a standard form that does not contain an references to current or parent directories.

normalizePath = (path, base=[]) ->
  base = base.concat path.split(fileSeparator)
  result = []

Chew up all the pieces into a standardized path.

  while base.length
    switch piece = base.shift()
      when ".."
        result.pop()
      when "", "."
        # Skip
      else
        result.push(piece)

  return result.join(fileSeparator)

loadPackage Loads a dependent package at that packages entry point.

loadPackage = (pkg) ->
  path = pkg.entryPoint or defaultEntryPoint

  loadPath(rootModule, pkg, path)

Load a file from within a package.

loadModule = (pkg, path) ->
  unless (file = pkg.distribution[path])
    throw "Could not find file at #{path} in #{pkg.name}"

  unless (content = file.content)?
    throw "Malformed package. No content for file at #{path} in #{pkg.name}"

  program = annotateSourceURL content, pkg, path
  dirname = path.split(fileSeparator)[0...-1].join(fileSeparator)

  module =
    path: dirname
    exports: {}

This external context provides some variable that modules have access to.

A require function is exposed to modules so they may require other modules.

Additional properties such as a reference to the global object and some metadata are also exposed.

  context =
    require: generateRequireFn(pkg, module)
    global: global
    module: module
    exports: module.exports
    PACKAGE: pkg
    __filename: path
    __dirname: dirname

  args = Object.keys(context)
  values = args.map (name) -> context[name]

Execute the program within the module and given context.

  Function(args..., program).apply(module, values)

  return module

Helper to detect if a given path is a package.

isPackage = (path) ->
  if !(startsWith(path, fileSeparator) or
    startsWith(path, ".#{fileSeparator}") or
    startsWith(path, "..#{fileSeparator}")
  )
    path.split(fileSeparator)[0]
  else
    false

Generate a require function for a given module in a package.

If we are loading a package in another module then we strip out the module part of the name and use the rootModule rather than the local module we came from. That way our local path won't affect the lookup path in another package.

Loading a module within our package, uses the requiring module as a parent for local path resolution.

generateRequireFn = (pkg, module=rootModule) ->
  pkg.name ?= "ROOT"
  pkg.scopedName ?= "ROOT"

  fn = (path) ->
    if typeof path is "object"
      loadPackage(path)
    else if isPackage(path)
      unless otherPackage = pkg.dependencies[path]
        throw "Package: #{path} not found."

      otherPackage.name ?= path
      otherPackage.scopedName ?= "#{pkg.scopedName}:#{path}"

      loadPackage(otherPackage)
    else
      loadPath(module, pkg, path)

  fn.packageWrapper = publicAPI.packageWrapper
  fn.executePackageWrapper = publicAPI.executePackageWrapper

  return fn

Because we can't actually require('require') we need to export it a little differently.

publicAPI =
  generateFor: generateRequireFn

Wrap a package as a string that will bootstrap require and execute the package. This can be used for generating standalone HTML pages, scripts, and tests.

  packageWrapper: (pkg, code) ->
    """
      ;(function(PACKAGE) {
        var src = #{JSON.stringify(PACKAGE.distribution.main.content)};
        var Require = new Function("PACKAGE", "return " + src)({distribution: {main: {content: src}}});
        var require = Require.generateFor(PACKAGE);
        #{code};
      })(#{JSON.stringify(pkg, null, 2)});
    """

Wrap a package as a string that will execute its entry point.

  executePackageWrapper: (pkg) ->
    publicAPI.packageWrapper pkg, "require('./#{pkg.entryPoint}')"

Require a package directly.

  loadPackage: loadPackage

if exports?
  module.exports = publicAPI
else
  global.Require = publicAPI

Notes

We have to use pkg as a variable name because package is a reserved word.

Node needs to check file extensions, but because we only load compiled products we never have extensions in our path.

So while Node may need to check for either path/somefile.js or path/somefile.coffee that will already have been resolved for us and we will only check path/somefile

Circular dependencies are not allowed and raise an exception when detected.

Helpers

Detect if a string starts with a given prefix.

startsWith = (string, prefix) ->
  string.lastIndexOf(prefix, 0) is 0

Creates a cache for modules within a package. It uses defineProperty so that the cache doesn't end up being enumerated or serialized to json.

cacheFor = (pkg) ->
  return pkg.cache if pkg.cache

  Object.defineProperty pkg, "cache",
    value: {}

  return pkg.cache

Annotate a program with a source url so we can debug in Chrome's dev tools.

annotateSourceURL = (program, pkg, path) ->
  """
    #{program}
    //# sourceURL=#{pkg.scopedName}/#{path}
  """

Return value for inserting into function for embedded windows.

return publicAPI

Definitions

Module

A module is a file.

Package

A package is an aggregation of modules. A package is a json object with the following properties:

  • distribution An object whose keys are paths and properties are fileData
  • entryPoint Path to the primary module that requiring this package will require.
  • dependencies An object whose keys are names and whose values are packages.

It may have additional properties such as source, repository, and docs.

Application

An application is a package which has an entryPoint and may have dependencies. Additionally an application's dependencies may have dependencies. Dependencies must be bundled with the package.