Modules/Async/B

From CommonJS Spec Wiki
Jump to: navigation, search

STATE: PROPOSED

Proposal for Modules/2.0. Common declarative module definition format, suitable for browser, server, human, and computer.

Rationale

This proposal is based on experience of development and maintenance of AJAX web application based on RequireJS (Modules/AsynchronousDefinition). There are some practical issues with AMD (and other CommonJS) module notation. In short - it's hard to read and thus it's hard to change, when compared with languages having native modules.

require( ["a", "b", "c", "d", "e"],
function( a, b, c, d, e ){
    return function(){
        a() + b() + c() + d() + e();
    }
});
  • It's not trivial to understand name-path correspondence.
  • Thus, it's error-prone to make changes in module import spec (add one module dependency, remove two others), if you have 5 or more dependencies, because you need to make coordinated changed in two lines of code.
  • This spec doesn't look like module spec in common languages. People who see it for the first time have no any idea what it is about and how to read it.

There are other 'synchronouns' Modules specs in CommonJS. Such as:

var c = require('CommonJS');

These approaches have two disadvantages. First, while they technically can be implemented in browser JS, there are problems to use script tag injection technique in these implementations. The last but not least of them is global namespace pollution. This is not good. Script tag injection is known to be the best technique for script loading in browser. One of important reasons for that is that script tag injection is friendly to in-browser debugger.

Second problem is that these approaches are imperative, while modules in most language are declarative specifications. Difference is that in first case you need to execute program in your mind to understand module's interface and dependencies, while in the second case you just read the description.

Some people might not treat this as problem. But it is. Declarative spec have fixed structure, thus it's dramatically easier to read, understand, parse, and change. In general, it is more high-level.

Can we introduce declarative modules definitions in language such as JavaScript, in the way which is friendly for script tags injection? No problems. It could be easily resolved with 'monadic' syntax:

// clearly state that it's module, not the plain .js file.
module.
 
// All necessary functions will be inside of 'module' namespace.
// Functions return module object, and needs to be chained.  
 
include( 'plain_1.js', 'plain_2.js' ). // load plain js scripts in order...
 
include( 'plain_3.js' ). // plain_3 will be loaded in parallel...
 
// import object specification.
// This object will be passed to the module's definition, when loaded.
imports({ 
   a: 'a', //easy to understand and change, since each name-path pair stands on separate line
   b: 'b',
   c: 'c',
   d: 'd',
   e: 'e'
}).
 
definition( function( _ ){ // import object with be passed as first argument
     module = function(){ // 'module' is overridden and used as export variable.
          return _.a() + _.b() + _.c() + _.d() + _.e(); // paths are replaced with asynchronously loaded module's values.
     }; 
});

This module definition syntax have following benefits:

  • It's more verbose than cryptic 'require' call, and in this case it's very good. This definition looks natural, much like native modules in mature languages.
  • The single name in global namespace is used, it's 'module'.
  • Clean and intuitive interface for plain js scripts inclusion, close to C/C++ #include directive. Each script will be loaded once.
  • 'monadic' syntax allow to force some certain section's order (say, 'includes' goes before 'imports', and nothing goes after 'definition').
  • it's easy to read and change dependencies.
  • it's easier to read the module's definition code, since mandatory prefix (in this case it's '_') makes it straightforward to distinguish imported and local resources.
  • Module's interface (exports) is searchible. It's important, since module's interface is the most interesting part of module definition.

PS: One more thing. It's easier to debug in node.js with this proposal. All dependencies will be loaded _before_ start of application code, allowing you to place breakpoints in any module.

You can't do that with traditional synchronous 'require' since modules will not be loaded for you in advance.

Specification

Module definition

  • Module definitions should always start with 'module.' statement.
  • Inside of module's definition, 'module' variable must be used as mandatory export object.
module.
definition( function(){
   var _private_data = 5;
 
   function privateFunction( a ){
      return _private_data += a;
   }
 
   module.inc = function(){
      return privateFunction( 1 );
   };
 
   module.add = function( a ){
      return privateFunction( a );
   };
});
  • module may export value of any type.
module.
definition( function(){
   var _private = 0;
 
   module = function( a ){
      return _private += a;
   };
});


  • Short form of module definition may be used:
module({
   get: function(){...},
   set: function( value ){...}
});

Importing other modules

  • Module definition may have imports section, which should be placed directly before 'definition' statement.
  • 'imports' statement takes single object with import spec. Import spec consists of 'import name': 'module path' pairs.
  • When all dependencies are loaded, import object is passed to definition function as 1st argument. Module paths should be substituted with actual imported values.
module.
 
imports({ 
   a: 'a',
   b: 'b/c',
   c: 'd/e/f'
}).
 
definition( function( _ ){
     module.something = function(){
          return _.a() + _.b() + _.c();
     }; 
});

Importing Custom Content (Content Plugins)

  • Module may import custom content, other than modules.
  • Custom content processing and loading must be handled by plugins.
  • Custom content import spec must contain object of the form {pluginName: 'path'} instead of path
module.
 
imports({ 
   a: 'a',
   b: {text: 'b/c.js'}, // import b/c.js as text file.
   c: 'd/e/f'
}).
 
definition( function( _ ){
     module.something = function(){
          return _.b + " - it's text";
     }; 
});

Import of plain js files

  • Module may require plain JS files to be loaded before method definition will be executed. In this case, 'include' statement is used, with file path as an argument.
  • 'include' statement must go right after 'module' statement.
  • There may be several include statements in module definition. The order of execution of loaded JS scripts is not guaranteed.
  • include statement may take several JS file paths as an argument. It's guaranteed that these scripts will be executed in sequence.
  • JS scripts, loaded with 'include' statements, must be executed only once.
  • It's not guaranteed that scripts loaded with 'include' will be executed before definitions of imported modules are evaluated.
module.
 
include('plain1.js').
include('plain2.js'). // order of execution of 1, 2, and 3 is not guaranteed. 
include('plain3.js', 'plain4.js'). // It's guaranteed that 4 will be executed after 3.
 
imports({
    some: 'module'
}).
 
definition( function( _ ){
    module.f = function(...){...};
    ...
});
  • 'preload' statement has the same semantic as 'include', with an exception that it is guaranteed that preloaded scripts will be executed before both included scripts and imported modules' definitions.
  • 'preload' statements must be placed before 'include'.

Path specs and options

  • module definition may contain path shortcuts spec, which will be used by loader in path resolution algorithm.
  • path shortcuts must be visible for all imported modules, and used in path resolution algorithm.
module.
paths({
    a: "b/c/d"
}).
imports({ some: 'a/module' }).
definition( function( _ ){
    module.f = function(...){...};
    ...
});
  • Module may override paths shortcuts, defined in upped modules.
  • Situation of shortcuts conflict (same path shortcut in the module, defined in two different upper modules) should be treated as error.
  • Module may define options, which are visible to all imported modules, and follow the same conflict detection rules as paths.
  • Options may affect behaviour of module loader and other software.
  • It should be impossible to change options from module's definition function.
module.
options({
   jQueryIsUsed: true
}).
paths({
    a: "b/c/d"
}).
imports({ some: 'a/module' }).
definition( function( _, options ){
    if( options.jQueryIsUsed )
        module.f = function(...){...};
    ...
});

Reference Implementation

It's wrapper around RequireJS functionality, implementing Modules/AsynchronousDefinition spec. In order to try include it with <script> tag just after require.js. Note, that at the top level you're using the same notation as described in this proposal. If you want to pass some init options to require.js, you can do it with 'options' statement.

Restrictions:

  • 'preload' statement is not implemented.
  • 'path' statement is not implemented, use 'options' path member instead. Options object is compatible with RequireJS spec.
  • recursive 'options' statement are not implemented. Options can be set only on the top level.
  • plain JS script loading should work, but have not been tested.
  • RequireJS plugins, and everything else should work.
var module = function(){
   var _options = {};
   var _firstCall = true;
 
   function Module(){
      var _include = [],//: []String
          _orderedInclude = [], //: []String
          _imports = [], //: []String
          _spec = {}; //: [String] ( String | [String] String )
 
 
      if( _firstCall ){ // options can be specified at the top level only...
         this.options = function( a_options ){
            _options = a_options;
            delete this.options; // no duplicate options specs...
            return this;
         }
      }
      //: fun(...: String )
      this.include = function(){
         if( arguments.length == 1 )
            _include.push( arguments[ 0 ] );
         else
            for( var i = 0; i < arguments.length; i++ )
               _orderedInclude.push( 'order!' + arguments[ i ] );
 
         return this;
      }
 
      this.imports = function( a_spec ){
         _spec = a_spec;
         for( var v in _spec ){
            var s = _spec[ v ];//: String | [ String ] String
 
            if( typeof s == 'string' )
               _imports.push( s );
            else
               for( var plugin in s )
                  _imports.push( plugin + '!' + s[ plugin ] );
         }
 
         delete this.include; // no include after imports
         delete this.imports; // no duplicate 'imports' statements
         return this;
      }
 
      this.definition = function( a_body ){
         var spec = _imports.concat( _include ).concat( _orderedInclude );
 
         function AMDWrapper(){
            var i = 0;
            for( var v in _spec ){
               _spec[ v ] = arguments[ i ];
               i++;
            }
 
            var theModule = module;
            module = {};
 
            a_body( _spec, _options );
 
            var exports = module;
            module = theModule;
 
            return exports;
         };
 
         var res = _firstCall ?
            require( _options, spec, AMDWrapper ) :
            define( spec, AMDWrapper );
 
         _firstCall = false;
 
         // initialize module namespace.
         module = function( a_object ){ // Short module definition
            return define( a_object );
         }
 
         var object = new Module();
         for( var x in object ){
            module[ x ] = object[ x ];
         }
 
         return res;
      }
   }
 
   return new Module();
}();