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.
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
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
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.
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
A module is a file.
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 arefileData
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
.
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.