(⬑Central whcl Documentation Hub) (⬑API Index)
require.whcl: A require.js-like Resource Loader
See also: the source code (in directory /dir/whcl/require.d/)
Jump to:
- Intro
- Basic Usage
- Modules vs. Plugins
- Search Paths and Extensions
- Testing Modules
- Random Tips and Tricks
Intro
require.whcl (we'll call it Require from here on out) is almost a clone of the requirejs JavaScript library. In short, Require is a "dependency loader." The caller provides a list of "dependencies," in the form of symbolic module names, Require loads them, and then passes all of the dependencies to a callback provided by the client (or, if no callback is provided, it returns the loaded dependencies to the caller). require.js simplifies dependency loading in JS immensely by hiding the asynchronous module loading behind a synchronous interface and ensuring that all dependencies are loaded before the client code is called. In whcl modules are not loaded asynchronously, but Require can nonetheless greatly simplify the creation of certain types of scripts, in particular when independent scripts can be used together in various combinations.
Incidentally, this model of resource loading is a perfect fit for cwal/whcl's lifetime model and garbage collector, ensuring optimal lifetimes for loaded resources (in particular, non-cached resources).
Require is comprised of a single script and a set of conventions:
TODO: add import-like feature to whcl:
Clients
eval
the require.whcl file contents. The (callable) object returned by that script is the Require loader, so clients should store it somewhere (in a const/var or property).Ideally,
require.whcl
lives in its own directory, under which loadable extensions are placed. This structure allowsrequire.whcl
, including its extensions, to be easily copied between source trees (yes, i do that). This documentation refers to that directory (whatever directory it getsimport()
'ed from) as Require's "home" directory, and it gets set as the "home" property of the Require object.When loaded, Require adds its home directory to its default search path. The intention (not requirement) is that shared Require-loaded files will be placed there, using subdirectories for organization. The current directory (".") is always added as the first search path, but clients are of course free to modify that.
Loading and Basic Usage
These docs will, for brevity's sake, assume that this module has been
installed and aliased with the name R
:
decl -const R [whcl.install-api require]
That alias is not strictly necessary. After calling
[whcl.install-api require]
, Require is globally available
via whcl.require
.
With that in place:
R [array module1 module2] [proc -anon {m1 m2} {
# … m1 and m2 are the results of eval'ing
# the files module1 resp. module2 …
}]
The first argument may be one of:
- An array of modules to load.
- A string naming a single module. This gets treated as if the caller had passed in a length-1 array of module names.
The second argument may be one of:
- A callback function.
- A block of script code.
If the 2nd arg is a function, it is passed one argument for each
entry in the list, in the same order they are imported. If it is a
code string then it is eval'd in a scope and the loaded modules are
made avilable in two forms via the modules
local var:
modules
is an array, and each index matches up with the indexes in the list passed to this function.The
modules
array also gets member properties set with the names matching those passed in in the list. (We cannot declare those names as local vars because the given names may well contain characters not legal for that purpose.)
Note that these callbacks may recursively invoke Require via a call to
whcl.require
. (It is often useful to do so, it turns out.)
It returns the result of calling the function (if any), the result of the eval'd code block, or the array of loaded modules (if passed no function/string).
The default behaviour is to cache each imported module, such that loading that module again will cause the same result value to be returned. This can be used to provide data sharing across invocations of the module. Plugins, described later, may change not only the caching behaviour, but also how files are searched, and some plugins don't work with files at all (we call those "virtual" plugins, for lack of a better term).
Alternately, Require can be called without a 2nd argument, in which case it returns an array containing the values which would have been passed to the callback:
decl mods [R.require [array mod1 mod2]]
# ^^^ still requires the list as a single array argument!
assert info [is-array $mods]; # just for demonstration
Modules vs. Plugins
Some terminology used heavily throughout the API:
Module is generically used to mean a script or other resource loaded by Require. Module references are strings like "moduleName" and "module/submodule/name".
Plugin, in Require, specifically means a proxy which changes how Require handles loading of a specific type of resource. Plugins allow it to not only load script code, but arbitrary resources, including arbitrary raw file content, database records, and whcl-loadable DLLs.
Some examples:
R [array (
"aModule"
"aPlugin!aModule"
"aPlugin!"
# ^^^^^^^^ without a module (only legal for some plugins)
"aPlugin!aModule?foo=hi there&bar=true&baz=3&faz"
)]
Plugins may offer configuration options. While they superficially
appear to be URL-encoded, they are not - no special encoding is used
except that a &
delimits key/value pairs. A key with no value is
treated as a boolean true
(under the assumption that it is a flag).
If Require cannot figure out what to do with an input string, or if loading a resource fails, it will throw (or propagate) an exception.
Modules
Modules, in the most basic sense, are simply whcl scripts which are,
for purposes of Require, expected to resolve to some value usable by
downstream code. In practice, modules tend to resolve to Objects,
Arrays, Functions, and the like, but in principle there is nothing
stopping a module from returning an integer, a boolean, a string, or
even null
or undefined
.
Here's an example of a trivial module which provides a couple of methods:
return object {
method-one [proc -anon {a} {return ($a + $a)}]
method-two [proc -anon {a} {return ($a * $a)}]
}
Pedantic sidebar: in Require module scripts, a "return" at the end is not strictly necessary because the final expression in a script is (unlike functions) its implicit result.
If we place that content in a file named my-module.whcl
somewhere in
Require's search path, we can then use it like this:
R my-module [proc -anon {my} {
assert 2 == [my.method-one 1]
assert 4 == [my.method-two 2]
}]
Or, with a script snippet as a callback:
R my-module {
decl my modules.my
assert 2 == [my.method-one 1]
assert 4 == [my.method-two 2]
}
Plugins
Plugins implement the actual "loading" of a resource. They are defined as an Object with a minimal interface documented in require.whcl's source code and summarized below.
Built-in Plugins
default
: handles non-plugin module calls and provides the file search paths for other plugins which use files but do not provide a search path of their own.nocache
: works like default, but bypasses the cache and does not cache the result.text
: resolves to the given file's contents as a String.buffer
: resolves to the given file's contents as a Buffer.
It also comes with examples of dynamically-loadable plugins in its source dir.
For completeness' sake, let's demonstrate how clients can create their own. First, let's create a plugin which returns entries from a hypothetical app-level configuration object.
Assume we have an application-level configuration object (somewhere!) with multiple levels of options. For example's sake, let's assume that object looks like:
whcl.install-api PathFinder
return object {
ui {
showLog true
disableAnimations true
}
resources {
iconLoader [new whcl.PathFinder
"/opt/myapp/resources/icons"
[array ".svg" ".png" ".xpm"/*[^3]*/]
]
}
}
A plugin can be added to Require using two different approaches. First,
it can be added to a file with the same name as the plugin (optionally
with a subdirectory component), with an .whcl
extension, and placed in the
directory REQUIRE_HOME/plugins
. Secondly, it can be passed to
R.add-plugin
.
Before demonstrating the implementation, let's show how the plugin should be used:
R 'myPlugin!configOptionName' [proc -anon {configOpt} {...}]
The R.get-plugin
method can be used to fetch (lazily loading, if
needed) a plugin object, but it is not expected that clients will ever
really need to do so except possibly to modify the search paths used
by them. In particular, modifying the path
and extensions
properties
(search path and extension lists, respectively) of the default plugin
changes the search path/extension list for any other non-virtual
plugin which does not define its own search path and/or extensions.
Now both installation approaches…
Contents of REQUIRE_HOME/plugins/myPlugin.whcl
:
return object {
is-virtual true /* means Require must not do file lookups for our plugin */
cache-it false /* means Require must not cache load() calls for this plugin */
config [getMyGlobalConfigObject], /* how you get this object is your business */
load [proc -anon {name opt} { /* called by Require when the plugin is used. */
/* this == the plugin object.
name is the "name" part after the "!" in the string passed by the client.
May be a falsy value (no name provided).
If the caller passes URL-style arguments (?a=b&c=...) then they are
provided as an object (key/value pairs) via the second parameter.
Passing options always bypasses the cache, because the options presumably
affect how the plugin behaves.
[^4] */
affirm [info is-string $name]
return this.config[ $name ]
}]
}
Or install it using R.add-plugin
:
R.add-plugin 'myPlugin' { … the plugin object … }
Of course, the above implementation could be enhanced to support
traversing sub-trees of the configuration, e.g. via
myPlugin!parent/child/option
, but that's beyond the scope of this
demonstration. Note, also, that the config object itself can be a
Require module, such that loading, e.g. the myConfig
module resolves
to the top-level configuration object. In fact, this particular use case
(serving config options) is arguably better served by a module (which
provides APIs to the client for fetching/modifying config data), as
opposed to a plugin, but... my imagination for creating a custom
plugin to demonstrate is failing me :/.
Search Paths and File Extensions
By default, modules are assumed to be base names of files (possibly
with a subdirectory component), and Require searches for them using a
plugin-dependent set of search paths and file extensions, defaulting
to those of the default plugin. Setting the is-virtual
plugin
property to a truthy value disables this - Require will then perform no
file lookup for the module name, and will pass it on as-is to the
plugin for handling (which might do its own file lookup!). The search
paths and file extensions are set via the path
resp. extensions
properties of a plugin (these properties are derived from the
PathFinder class, which is used to
search for files, as it uses those naming conventions). If a plugin is
not virtual but has no path
/extensions
properties of its own, those
of the default plugin are used.
See require.whcl (search for "path" and "extensions") or the dynamically-loadable plugins for several examples of setting up paths and extensions.
By default, the search path includes the current directory and
Require's home directory, and the only file extension used by default
is ".whcl"
. Clients are of course free to modify both of those lists
(whcl does not require any specific file extension for script files,
of course).
Generic modules which have no dependencies on project-local code
"should" be placed under Require's home directory, using subdirectories
to group functionality. One useful convention for is
ProjectName/FeatureSet/FeatureName.whcl
. For example, "myProj/db/users"
or "myProj/dogs/puppy"
.
The main require.whcl source dir has examples of modules and plugins, demonstrating some semblance of structure.
Testing Modules
The canonical whcl source tree includes a shell script which simplifies the testing of Require modules. To create a module test, place a file in the same directory as the module, with the same base name but with a ".test.whcl" extension. A typical test script Requires the module(s) it is testing and throws an error (or asserts) on failure. Here's an example session:
[stephan@host:~/fossil/cwal/whcl]$ ./r-tester.sh
Individual tests can be run by passing one or more module names to the
test script (without the .test.whcl
extension part), and valgrind
tests can be run by passing -vg
to the script1, e.g.:
$ ./r-tester.sh -vg moduleName1 moduleName2
Pass it -?
to see the full help text.
Random Tips and Tricks
Fetching a single module using conventional call semantics is simple:
decl -const module [R 'moduleName'].0;
Footnotes
- ^ Noting that valgrind may well report that the system-level module loader leaks memory. Not my fault.
-
[^
3
]XPM: don't knock it 'til you've tried it!
-
[^
4
]whcl pro tip: add such comments outside of function bodies to save memory!