/**
Demonstrates a basic script module loading API similar to
require.js (http://requirejs.org).
In short, this module acts as a loader for arbitrary other content,
be it s2 script code, raw file content, or "virtual" (non-file)
content plugged in by the user. Its usage is modelled very much of
off requirejs, and it will look very familiar to anyone who has
used that library.
See the API docs in this module for more details. For even more
details (or other details, in any case), see this public Google
Doc:
https://docs.google.com/document/d/14gRP4f-WWgWNS64KM_BI7YjQqBLl4WT3jIrmNmFefYU/view
Example usage:
Assume the files r1.s2 and r2.s2 live somewhere in this module's
search path and that both return an Object with a foo() method.
This is how we might import and use them:
@code
const R = s2.import('require.s2');
R(['r1', 'r2'], proc(r1, r2){
r1.counter = r2.counter = 1;
print(__FLC,r1);
print(__FLC,r2);
r1.foo();
r2.foo();
++r1.counter;
++r2.counter;
});
R(['r2', 'r1'], proc(r2, r1){
assert 2 === r2.counter;
assert 2 === r1.counter;
// ^^^ because imported script results are cached
});
@endcode
*/
affirm s2.io;
affirm 'string'===typename s2.io.dirSeparator;
affirm 'function'===typename s2.io.realpath;
affirm 'function'===typename s2.Buffer.readFile;
affirm 'function'===typename s2.PathFinder.new;
return scope {
// TODO? Refactor to use Object.withThis() instead of a scope.
const importFileBuffer = s2.Buffer.readFile;
const importFileText = proc(fn){
return importFileBuffer(fn).takeString();
}.importSymbols(nameof importFileBuffer);
/**
Internal utility to convert almost-URL-encoded plugin options
to some value form. Treats non-existent values (===undefined)
as a boolean true flag.
*/
const convertQValue = proc(v){
undefined === v && return true;
'true' === v && return true;
'false' === v && return false;
'null' === v && return null;
var x = 0.parseNumber(v);
undefined!==x && return x;
return v;
};
/**
This "default" plugin (A) acts as the default
implementation for fetching require()'d files, (B) is
where the interface for other plugins is documented.
*/
var PluginModel ={
/**
Clear the prototype - we don't really need it for
this object.
*/
prototype: undefined,
/** If true, require() does not search for the filename it
is given. (It is assumed to be some form of virtual
unique identifier, and may be empty.)
*/
isVirtual: false,
/**
cacheIt tells require whether or not to cache the results
of requests to this plugin. If it is true, then this.load()
will only be called for the first request, and its result
will be returned on subsequent requests. EXCEPTION: if
a plugin is called with arguments (see load()) the cache
is ignored/bypassed.
The default is expected (by client code) to always be true
for the default plugin!
*/
cacheIt: true,
/**
For file-based plugins, prefix specifies the search
directories (array of strings, excluding any trailing slash
on the directory parts unless they refer to the root
directory).
*/
prefix: ('string' === typename var tmp = s2.getenv('S2_REQUIRE_PATH'))
? tmp.split(tmp.indexOf(';') >= 0 ? ';' : ':') : ['.'],
/**
For file-based plugins, suffix specifies the search
extensions (array of strings, including any '.' part,
e.g. use ".foo" instead of "foo" unless you're searching
for partial extensions).
*/
suffix: ('string' === typename tmp = s2.getenv('S2_REQUIRE_EXTENSIONS'))
? tmp.split(tmp.indexOf(';') >= 0 ? ';' : ':') : ['.s2'],
/**
Called by require() to "load" a given module. load() must
accept a module name, "load" that module, for a given
definition of "load", and return its contents or
result. (It need not actually load anything from anywhere,
much less a file: it might simply return a predefined
object or other value.)
In the call context, 'this' will be this plugin
configuration object.
The first argument is the "name" part of the string passed
to require.
The second argument part is either undefined or an object:
if the file string contains '?', anything after the ? is
assumed to be encoded in the form a=b&c=d... (URL-like, but
_without_ the URL encoding), and that gets transformed into
an Object of key/value pairs before passing them to this
function. HOWEVER: passing any arguments after '?' will
cause caching to be bypassed, because the arguments presumably
change how the plugin works. Note that if nothing follows
the '?' then no options object is created.
Caveats regarding the opt parameter:
1) all of the values in the opt object will originally be
of type string, but numeric-looking strings and the strings
("true", "false", "null") get converted to their
script-native type.
2) Only one level of opt object is supported, not nested
objects, arrays, etc.
*/
load: proc(fn/*, opt*/){
return s2.import(fn);
}.importSymbols(nameof s2)
/* Reminder to self: we cannot simply alias to s2.import() because
any extra non-string arguments would be passed to it (doing the
wrong thing). Reminder #2: adding comments outside of function
bodies, instead of if them, uses less memory, since they don't
get allocated as part of the function body. That's especially
significant when the length of the comments outweigh the rest
of the source, as in this case. */
;
};
/**
The main "require" module object.
*/
var mod = {
__typename: 'require.s2',
/**
Used for doing file lookups. Its search path and extensions
get continually swapped out by require() and friends.
*/
pf: s2.PathFinder.new()
};
/**
The funkiness we do with 'this' vs 'mod' in may places places
below is so that this stuff still works when clients copy the
returned module into a property of another object. i.e. the following
usages are equivalent:
var x = mod;
obj.x = mod;
x([...],...);
obj.x([...],...);
Both calls (because of these extra bindings) use the same "this"
inside the call - the local 'mod' symbol, which is important for
identical/correct semantics in both uses.
*/
/**
All the plugins are stored here, and any number may potentially
be loaded (and added here) via client-side use.
*/
mod.plugins = {
/**
The name 'default' is magic and assumes certain
plugin-level defaults. It also provides the default .prefix
and .suffix properties for other non-isVirtual (file-using)
plugins (used only if a given plugin does not define them
itself).
*/
default: PluginModel,
/**
Works just like the default plugin but bypasses
the cache.
*/
nocache: {
cacheIt: false,
load: PluginModel.load
},
/** Loads file content as a string. */
text: {
cacheIt: false,
// prefix: uses the defaults
suffix: ['.txt', '.s2', '.html'],
load: importFileText
},
/** Loads file content as a buffer. */
buffer: {
cacheIt: false,
// prefix: uses the defaults
suffix: ['.txt', '.s2', '.html'],
load: importFileBuffer
}
/**
Demonstration of a "virtual" plugin (one which does not
use files).
virtualDemo:{
isVirtual: true,
cacheIt: true, // not strictly necessary, plugin-dependent
load: proc f(fn,opt){
print("Example of a 'virtual' handler. Arguments:",argv);
return opt ||| this;
}
}
*/
};
/**
Installs one or more plugins into this object.
If called with an initial string arugment then:
- Adds a new plugin. name must be a string and pluginObj must
be an object which follows the PluginModel interface. Returns
pluginObj on success.
If called with a non-string argument then:
- name is assumed to be a container. Each of its properties is
assumed to be a module definition, and each one gets installed
via a call back into this function, passing it each key and
value. Returns this object.
Throws on error.
*/
mod.addPlugin = proc callee(name, pluginObj){
if(argv.0 && !argv.1 && 'string' !== (typename name) && mod !== argv.2){
affirm s2.isDerefable(name,true) /* expecting a container value */;
mod.eachProperty.call(name, eachProxy);
return this;
}
affirm 'string' === typename name;
affirm name /* name must be non-empty */;
affirm s2.isDerefable(pluginObj,true) && s2.isCallable(pluginObj.load);
mod.plugins[name] = pluginObj;
return mod;
}.importSymbols({
mod: mod,
eachProxy: proc(k,v){callee.call(mod, k, v, mod)}
});
/**
Searches for a plugin script by using this.plugins.default's
search path and the name ("plugins/"+name). Returns undefined
if not found, else the result of s2.import()'ing that file. It
does not check if the plugin is already installed, but installs
(or overwrites) it into mod.plugins[name].
*/
mod.searchAndInstallPlugin = proc(name){
mod.pf.prefix = mod.plugins.default.prefix;
mod.pf.suffix = mod.plugins.default.suffix;
var fn = mod.pf.search('plugins/'+name);
return fn
? mod.plugins[name] = ((const requireS2=mod), s2.import(s2.io.realpath(fn)))
: undefined;
}.importSymbols(nameof mod),
/**
If the given plugin name is already installed, it is returned,
otherwise it is sought for, installed, and returned. An
exception is thrown if it cannot be found or if installing it
fails.
*/
mod.getPlugin = proc(name){
//print(__FLC,'plugin',name, mod.plugins[name]);
return mod.plugins[name] ||| mod.searchAndInstallPlugin(name);
}.importSymbols(nameof mod),
/**
Given a base filename, it searches for the file using the
configured search paths/extensions (from this.plugins). If
found, it is passed to the import() function specified
for the import type (see below).
By default this.plugins.default is used to search for and
import the file. If a "special" type of name is provided to
this function, though (meaning the base name looks like
with "SOMETHING!basename"), then this.plugins[SOMETHING] is
used (if set), which may change the caching behaviour and
how the content of the file is interpreted.
Depending on the configuration options, requests might get
cached. Subsequent calls which expand to the same file name
will return the same (cached) result value on each
subsequent calls.
*/
mod.import = proc(basename){
affirm 'string' === typename basename;
var pluginName;
/* Check for pluginName!... */
if((var ndx = basename.indexOf('!'))>0){
pluginName = basename.substr(0,ndx);
basename = basename.substr(ndx+1);
}
pluginName || (pluginName ='default');
/* Configuration for this plugin... */
const requireS2 = mod /* public API symbol, potentially
needed by anything which
uses anyPlugin.load() */;
var pConf = mod.getPlugin(pluginName);
const pf = pConf.isVirtual ? undefined : mod.pf /* PathFinder */;
if(pf){
// Set up file lookup paths/suffixes...
pf.prefix = pConf.prefix ||| mod.plugins.default.prefix;
pf.suffix = pConf.suffix ||| mod.plugins.default.suffix;
}
/* Treat ?a=b&c=d... almost like a URL-encoded
as query string, but without the URL encoding.
*/
var qArgs, useCache = pConf.cacheIt;
if(basename.indexOf('?')>=0){
/* Parse args. If any are provided, bypass the cache. */
var sp = basename.split('?',2);
basename = sp.0;
(qArgs = splitQArgs(sp.1))
&& useCache && (useCache = false);
}
/* Find the file, if necessary... */
var fn = pConf.isVirtual
? basename
: (basename ? pf.search( basename ) : false)
||| (throw "Cannot find '%1$s' in search path %2$J with extensions %3$J.".
applyFormat(basename, pf.prefix, pf.suffix))
/* s2 "pro tip": the extra parens around the throw are a
tokenization/evaluation optimization (requires less
work for the evaluator than skipping over a non-grouped
expression). String.concat() is also more efficient for
more than two strings than using (string + string).
Pro tip 2: comments in function bodies cost memory, as
they are allocated along with the function body. There
goes 39 more bytes.
*/;
// expand to the fully qualified path for non-virtual plugins...
pConf.isVirtual || (fn = s2.io.realpath(fn));
//print(__FLC,"fn=",fn,"useCache=",useCache,"qArgs =",qArgs,pluginName,pConf);
//print(__FLC,'pConf.cacheIt=',pConf.cacheIt,', fn=',fn);
//print(__FLC,cache.toJSONString(2));
return useCache
? (const key = (pConf.isVirtual
? pluginName
: pluginName ? pluginName+'!'+fn : fn),
cacheCheck = (cache # key))
? cacheCheck
: cache.insert(key, pConf.load(fn, qArgs))
: pConf.load(fn, qArgs);
}.importSymbols({
mod: mod,
cache: s2.Hash.new(0.nthPrime(10)),
splitQArgs: proc(str){
str || return str;
var rc;
str.split('&').eachIndex(proc(v){
var sp = v.split('=',2);
(rc ||| (rc = {}))[sp.0] = sp.1 ? convertQValue(sp.1) : undefined;
});
return rc;
}.importSymbols(nameof convertQValue);
});
/**
For each base filename in the given array, this function calls
this.import(basename) and appends the results to a new array.
Returns an array of the results to each import() call, the
order of the array's elements being the same as the calls to
import(). Throws if list is not an array or is empty or if loading
any resources fails. Propagates
any exceptions
*/
mod.importList = function(list){
affirm list inherits argv.prototype /* expecting an Array */;
const n = list.length();
affirm n>0 /* expecting a non-empty list of module names */;
const imps = [];
for( var i = 0; i < n; ++i ) imps.push(mod.import(list[i]));
return imps;
}.importSymbols(nameof mod);
/**
Imports a list of script modules and calls a callback after
loading all of them.
list is an array of strings - script file base names to import.
func is a function which gets called after importing all of the
scripts. It is passed one argument for each entry in the list,
in the same order they are imported.
Example:
assert 42 === thisObj(
['module1', 'module2'],
function(mod1, mod2){
print('result of module1 import:' mod1);
print('result of module2 import:' mod2);
return 42;
}
);
ACHTUNG: The require() mechanism function "exports" the module
loader instance into the call chain via the const symbol
'requireS2', the intention being that imported modules can
(because of s2's funky symbol lookup) use this symbolic name to
recursively call into require() if needed (it is often useful
to do so, it turns out, and this consolidates the convention
across modules originally written for different code bases).
_Potential_ TODOs:
Alternate usages:
- require('name', 'name', ..., function )
- If passed a non-string value where a name is expected, use it
as-is as the result of the loading. i suspect we might have
some interesting uses for that, but want a use case before trying
it out.
*/
mod.require = function callee(list, func){
func && affirm func inherits callee.prototype /* is-a Function */;
return func
? ((const requireS2=mod),
func.apply(func, mod.importList(list)))
: mod.importList(list);
}.importSymbols(nameof mod);
if(var d = __FILEDIR ? s2.io.realpath(__FILEDIR) : 0){
// Add this file's directory to some lookup paths.
mod.plugins.default.prefix.push( mod.home = d );
}else{
/* __FILEDIR may be a synthetic __FILE name
(e.g. via eval or Buffer.evalContents()) */
mod.home = "";
}
/* We set mod.require.home so that plugins can construct paths via
requireS2.home. */
/* Make it so that call()ing this object calls mod.require() */
mod.prototype = mod.require /** Holy cow! We've just inherited our
own member function. */;
mod /* scope result */;
};