Login
Artifact [bfa8d5167f]
Login

Artifact bfa8d5167f31feeebc81aefae25b8bfa09f0f9ce:


/**
   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 */;
};