JavaScript package and module system for interactive development.
Module and package system
Live and interactive devlopment
Goals
lively.modules provides a framework for loading, defining and interactively modifying JavaScript modules. A module is an entity containing JavaScript source code that adheres to the definition of the ECMAScript Language Specification.
For an intro to the topic see ES6 In Depth: Modules at mozilla.org
Its main purpose is to
- Provide an interface to load modules and groups of modules (packages)
- Provide an interface to access and modify the runtime state of a module, i.e. its
- dependencies (modules imported and modules that import it)
- imported and exported values
- source code
- internal definitions
- Provide a user friendly and practical implementation of how imported modules are resolved *.
- For the purpose of grouping modules together and providing a method for module lookup introduce a lightweight concept of a package.
* The ES specification explicitly leaves the semantics for "HostResolveImportedModule" open to module implementations
For more please see doc/rationale.md.
Usage
To load lively.modules you can use the pre-build
dist/lively.modules-with-lively.vm.js
file. Once that happens the
lively.modules
global will provide an interface for loading packages,
modifying modules, evaluating source code in module contexts etc.
So on a webpage you would typically link via
<script src="../node_modules/lively.modules/dist/lively.modules-with-lively.vm.js"></script>
See the examples in lively-system-examples for more details.
API
main interface
lively.modules.importPackage(packageName)
To load a project into your runtime you will typically use
lively.modules.importPackage('some-package-name')
. 'some-package-name'
should resolve to a directory with a JSON package config file (typically
package.json) that at least defines a name
field. The package will be
imported, i.e. the main module of the package will be loaded via
lively.modules.System.import('some-package-name/index.js')
. By default the
name of main is 'index.js'
but this can be customized via the main
field
of the package config file.
The result of the importPackage call is the promise for loading the main module.
Specifics of the lively package format
The main purpose of the lively package format is to make it easy to integrate
dependent packages in the lively.module and es6 module systems. It allows you
to define a "lively"
field in the main JSON that allows to set a separate
main module, a "packageMap"
object that maps names that can be used in
import
statements to directories of sub-packages. When sub-packages are
discovered while importing a package, those are recursively imported as well.
Here is an example how a config inside a package.json file could look like.
{
"name": "some-package",
"main": "main-for-non-es6.js",
"lively": {
"main": "for-es6.js",
"packageMap": {
"dep1": "./node_modules/dep1",
"dep2": "./libs/dep2"
}
}
}
For more examples, see lively.modules/package.json, or lively.ast/package.json.
lively.modules.System
The main lively.modules interface provides access to a System loader object (currently from the SystemJS library that has some improvements added, e.g. the name normalization respects the lively package conventions, translate is used to instrument code by default, etc.
By default the loader instance is the same as the global loader (e.g. window.System). Note: The System instance can be easily changed to support multiple, isolated environnments.
Example:
var testSystem = lively.modules.getSystem("my-test-system");
lively.modules.changeSystem(testSystem, true); // true: make the System global
System.import("some-module"); // uses the new System loader
Now all state (what modules are loaded, their metadata, etc) are stored in
testSystem
. Changing to another System allows to define different name
resolution approach etc.
Side note: Since all System related implementation functions defined in the modules in src/ will take a System loader object as first parameter, the implementation is loader independent.
Loader state / module state
-
lively.modules.loadedModules()
: Returns a list of ids of the currently loaded modules. -
lively.modules.printSystemConfig(): Returns a stringified version of the SystemJS config. Useful for debugging SystemJS issues
lively.modules.requireMap()
Will return a JS object whose keys are module ids and the corresponding values are lists of module ids of those modules that dependent on the key module (including the key module itself). I.e. the importers of that module.
instrumentation
By default lively.modules will hook into the System.translate
process so that source code of modules get transformed to allow recording of their internal evaluation state (that is then captured in moduleEnv
s). You can enable and disable this behavior via
lively.modules.wrapModuleLoad()
lively.modules.unwrapModuleLoad()
evaluation
- This is handled by the lively.vm module!
ModuleInterface
lively.modules.module(moduleId)
Returns an instance of ModuleInterface with the following methods:
ModuleInterface>>dependents()
Which modules (module ids) are (in)directly import module with id.
Let's say you have
- module1.js:
export var x = 23;
- module2.js:
import {x} from "module1.js"; export var y = x + 1;
- module3.js:
import {y} from "module2.js"; export var z = y + 1;
module("module1.js").dependents()
returns [module("module2"), module("module3")]
ModuleInterface>>requirements()
which modules (module ids) are (in)directly required by module with id?
Let's say you have
- module1:
export var x = 23;
- module2:
import {x} from "module1.js"; export var y = x + 1;
- module3:
import {y} from "module2.js"; export var z = y + 1;
module("module3").requirements()
will report [module("module2"), module("module1")]
async ModuleInterface>>changeSource(newSource, options)
To redefine a module's source code at runtime you can use the
changeSource method. Given a.js
from the previous example you can run
module('a.js').changeSource('var x = 24;\nexport x;')
.
This will a) evaluate the changed code and b) try to modify the actual file
behind the module. In browser environments this is done via a PUT
request,
in node.js fs.writeFile
is used.
`async ModuleInterface>>reload(options)``
Will re-import the module identified by moduleName
. By default this will
also reload all direct and indirect dependencies of that module. You can
control that behavior via options
, the default value of it is
{reloadDeps: true, resetEnv: true}
.
ModuleInterface>>unload(options)
Will remove the module from the loaded module set of lively.modules.System.
options
are by default {forgetDeps: true, forgetEnv: true}
.
async ModuleInterface>>imports()
and async ModuleInterface>>exports()
Import and export state. For exports this includes the local name of the exported variable, its export name, etc. For imports it includes the imported variable name, the module from where it was imported etc.
Example:
await module("lively.modules/index.js").exports();
// =>
// [{
// exported: "getSystem",
// local: "getSystem",
// fromModule: "http://localhost:9001/node_modules/lively.modules/index.js",
// }, ...]
await module("lively.modules/index.js").imports();
// [{
// fromModule: "lively.lang",
// local: "obj",
// localModule: "http://localhost:9001/node_modules/lively.modules/index.js"
// }, {
// fromModule: "./src/system.js",
// local: "getSystem",
// localModule: "http://localhost:9001/node_modules/lively.modules/index.js"
// }, ...]
// })
async ModuleInterface>>source()
Returns the source code of the module.
async ModuleInterface>>env()
Returns the evaluation environment of the module.
A "module env" is the object used for recording the evaluation state. Each
module that is loaded with source instrumentation enabled as an according
moduleEnv It is populated when the module is imported and then used and
modified when users run evaluations using lively.vm.runEval()
or change the module's
code with ModuleInterface>>changeSource()
. You can get access to the internal module
state via module(...).env().recorder
the recorder is a map of
variable and function names.
Example: When lively.modules is bootstrapped you can access the state of its main module via:
var id = System.decanonicalize("lively.modules/index.js");
Object.keys(lively.modules.moduleEnv("lively.modules/index.js").recorder);
// => ["defaultSystem", "changeSystem", "loadedModules", "sourceOf", "moduleEnv", ...]
lively.modules.moduleEnv("lively.modules/index.js").recorder.changeSystem
// => function() {...} The actual object defined in the module scope
hooks
lively.modules provides an easy way to customize the behavior of the System
loader object via installHook
and removeHook
. To extend the behavior of
of lively.modules.System.fetch
you can for example do
installHook("fetch", function myFetch(proceed, load) {
if (load.name === "my-custom-module.js") return "my.custom.code()";
return proceed(load); // default behavior
});
notification
There are five types of system-wide notifications:
{type: "lively.modules/moduleloaded", module}
{type: "lively.modules/modulechanged", module, oldSource, newSource, error, options}
{type: "lively.modules/moduleunloaded", module}
{type: "lively.modules/packageregistered", package}
{type: "lively.modules/packageremoved", package}
These notifications are all emitted with lively.notifications
.
Development
To bootstrap lively.modules please see the example in examples/bootstrap/. lively.modules is completely capable to "develop itself" and was done so from the beginning :)
To build a new version yourself run npm run build
.