- Notifications
You must be signed in to change notification settings - Fork0
Simple to use asynchronous resource (javascript, CSS, ...) loader and dependency resolver for Javascript apps. Extensible plugin system. Support for bundled javascript assets and more.
License
webdevelopers-eu/javascript-dna
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Table of Contents
- Javascript DNA / jDNA
Unobtrusive, simple to use, asynchronous script loader and dependency resolver that will
- dramaticallyoptimize the loading speed of many scripts
- bringorder into big web apps
- allow you to defineclean Javascript classes (prototypes) the way you always wanted to - without
define()
ormodule.exports
auxiliary trash - ability to resolve dependencies for all Javascript files CSS files andmore
Q: Why another AMD solution? We have Dojo Toolkit, RequireJS, and ScriptManJS...
A: There was a need for modern,clean andintuitive loader. What every programer needs is a system that you can say to "I need classes
Class1
andClass2
for my code to work." System will somehow make that happen and then execute your dependent code. This is what everybody wants. And this is what most f(r)ameworks don't get right.Programmers are required to alter their precious code to enable loading frameworks. That I consider in most cases as pure evil. Framework should help you and you should not be required to help framework. Do you need to include
define()
at the end of your PHP class definition file? No! Why should you in Javascript?The desire was to build the system that will understand simple files containing clean (future ECMA6) class declarations or current Javascript prototype definitions without any
define()
andmodule.export
trash code. Something you are used to from other languages. Something where one can express dependencies usingclass names rather then cryptic ids or file paths... Something like
// Contents of file /just/anywhere/file.jsfunctionPoint(x,y){this.coord=[x,y];}Point.prototype.toString=function(){return'['+this.coord.join(' x ')+']';};
that can be intuitively required in the code:
dna('Point',// I need the Point prototypefunction(){// Run this after you load the Point prototypenewdna.Point(10,20);});
There was nothing like that. Most solutions failed to meet this simple expectation of every programmer by making impractical design choices or implementing unnecessarily complex solutions to achieve simple goal.
If you want to try somethingsimple,intuitive and verypowerfull thenJavascript DNA is here for you!
- Simple API - all you need is just one method
dna(...)
. That's really it. There is not more to it. - Bundles - optional script archives capable of accommodating dozens of scripts that are fast to download and don't carry unnecessary burden on browser's internal script parser.
- CSS resources - you can load javascripts as easy as CSS resources only when needed.
- 100% asynchronous - scripts are loaded out-of-order yet evaluation order is guaranteed.
- Out-of-order API - with our unique O₃ API (Ozone API) you can call DNA methods in any order, define your hooks with their requirements before feeding DNA with dependency information, call DNA even before it is loaded and more. No need to worryif,when or inwhat order you can use any of DNA features.
- Optimized - small and fast with minified size of just about 11kB.
- Easy debugging - shows correct source/lines in debuggers. Reporting problems in console. Global error handlers.
- Open plugin system - support for URL-rewritting, download and script evaluation plugins.
- jQuery based
Include DNA script on your page
<scriptsrc=".../dna.js"></script>
Define locations of your Javascript prototypes and their dependencies
dna({'proto':'MyPrototype1',// Prototype name that you use in your code requirements'require':'MyPrototype2',// It requires also another prototype defined elsewhere'load':'/somewhere/func1.js'// There is MyPrototype1 expected to be defined.},{'proto':'MyPrototype2','load':'/somewhere/func2.js'});
Create files with your javascript classes:
File/somewhere/func1.js
functionMyPrototype1(){newdna.MyPrototype2;// we expect DNA to resolve also MyPrototype2 requirement}
File/somewhere/func2.js
functionMyPrototype2(){alert('Hello world!');}
Execute your function after DNA exports explicitly requiredMyPrototype1
and its dependencyMyPrototype2
asdna
properties:
dna('MyPrototype1',function(){newdna.MyPrototype1;});
dna( [ REQUIREMENT | CONFIGURATION | CONFIGURATION_URL | CALLBACK | ARRAY | SETTINGS ], ...):Promisedna.push( REQUIREMENT | CONFIGURATION | CONFIGURATION_URL | CALLBACK | ARRAY | SETTINGS ):Number
REQUIREMENT
:String
is a string withid
,proto
,service
identifier that needs to be resolved before calling callbacks.CONFIGURATION
:Object
is an object with list of requirements and scripts to load. See more inConfiguration section.CONFIGURATION_URL
:String
you can store your configuration(s) as an array of objects in an external JSON file. This will load configurations from the file. JSON URL must contain at least one character "/
" (e.g. "./dna.json
") Note: Listed URIs will berewritten anddownloaded using plugin system. JSON files can contain also string names of prototypes/services to be loaded right away.CALLBACK
:Function
any callback(s) to be executed when all requirements were resolved. Same as specifying callback using$(...).done(CALLBACK);
ARRAY
:Array
list of any combination of items of typeREQUIREMENT
|CONFIGURATION
|CONFIGURATION_URL
|CALLBACK
|ARRAY
|SETTINGS
.SETTINGS
:Object
see more inSettings andCore Plugin System section.
Returned values
Promise
- the call todna(...)
always returns jQueryPromise object that you can use to hook your callbacks onto immediately or anytime later.Number
- the call todna.push(...)
returns the size of queue of commands waiting for resolution.
Calldna.push()
only if you are not sure if DNA is loaded. See sectionCall DNA Before It Loads. Good practice is to call it with a callback without any dependencies specified. This will call the callback immediatelly after DNA is loaded.
vardna=dna||[];dna.push(function(){// On DNA loaddna(…);…;});
Configuration Objects are used to define dependencies and requirements.
{'id':ID,'proto':PROTO,'service':SERVICE,'data':DNAME,'require':REQUIRE,'load':LOAD,'eval':EVAL,'context':CONTEXT}
Where
ID
:Identifier
Optional. Unique super-identifier (unique across allid
,proto
,service
identifiers in all configurations).PROTO
:Identifier
Optional. A super-identifier. Name of theFunction
javascript object. Must start with an upper-case letter. This object will be available asdna
property (e.g.dna[PROTO]
) after successful resolution. SeePrototype Aliases to see how to load multiple versions of the same script.SERVICE
:Identifier
Optional. A super-identifier. Name of the propertydna[SERVICE]
. Must start with a lower-case letter. Thedna[SERVICE]
will be populated with object created usingPROTO
Function
(in a nutshell it will dodna[SERVICE]=new dna[PROTO];
).DNAME
:Identifier
Optional. A super-identifier. Name of the propertydna.data[DNAME]
. Ifrequire
section contains any JSON URIs (e.g. "json:./my-config.json"), deserialized JSON objects will be merged in that property. Thedata
identifier can be specified more then once.REQUIRE
:URI|Identifier|Array
Optional. One or array ofid
,proto
orservice
identifiers that define dependencies or relative or absoluteCONFIGURATION_URL
of file with additional list of JSON-serializedCONFIGURATION
objects to be loaded. All dependencies referred by listed super-identifiers will be resolved prior to resolvingload
section of this particular configurationLOAD
:URI|Array
Optional. A list of absolute or relative (resolved to a containing.json
file or current document) URLs of Javascript or HTML (seeBundled Assets) files to be loaded and parsed/executed. Files are guaranteed to be executed in listed order with required dependencies executed first. Note: Listed URIs will berewritten anddownloaded using plugin system.EVAL
:String
Optional. Accepted values:dna
(default) or custom name. See more inEvaluation Engines section.dna
evaluates the script in closure scope and expects the script to define variable of name specified in configuration'sproto
property.deferred
your script is not expected to define variable of name specified inconfig.proto
property but you are expected to pass object representingconfig.proto
to Deferred object stored infactory
variable.- custom name expects you to specify your own factory to execute the code and return the result object. See more inCustom Evaluation Engines section.
CONTEXT
:String
Optional. Default:false
. Name of the context to evaluate the script. Currently supported values: "window
" orfalse
.false
(default) boolean causes the script evaluation in its own context.- "
window
" string causes evaluation inwindow
object context. STRING
Experimental - any name identifying a shared context. Scripts having the same context name will havethis
andcontext
set to the same private Object. SeeNamed Context section for more information.
Note: At least oneid
orproto
super-identifier must be specified in the single Configuration Object.
Just pass the Configuration Object or URL pointing to JSON file with Configuration Objects todna()
method. Seesyntax section for more. Examples:
dna('/dna.json');dna({'proto':'Test','load':'/file.js'});dna('/dna.json',{'proto':'Test','load':'/file.js'},['/other.json','/otherother.json']);
You can also include other JSON configuration files from withing Configuration Object.
[{"id":"load-big-project","description":"Huge Project included on request.","require":"./my/big-project.json"},{"proto":"MyObject","description":"My code depending on Huge Project using indirect `require`","require":['load-big-project','ClassFromBigProject'],"load":"object.js"},{"id":"MyObject2","description":"My code depending on Huge Project using direct `require`","require":["./my/big-project.json","Class2FromBigProject"],"load":"object2.js"},{"id":"MyObject2","description":"My code depending on Huge Project using `load`","require":[],"load":["config:./my/big-project.json","object2.js"]}]
You can then load additionalbig-project.json
just by requiringload-big-project
. E.g.dna("load-big-project").done(...);
or indirectly throughrequire
statement usingdna("MyObject").done(...);
. Thatway you can confortably break huge projects in multiple JSONconfigurations and set up dependencies between them.
It is also possible to load external JSON usingconfig
scheme - see more inInbuilt Config Scheme section.
You can use DNA to load JSON configuration files. Example of DNA configuration.
{"service":"myService","proto":"MyClass","load":["./myclass.js"],"require":["myConfig"]},{"data":"myConfig","load":["json:./configs/my.json"]}
After runningdna("myService")
following happens:
myclass.js
is fetched and it is expected to containMyClass
declaration that will be stored indna.MyClass
property.my.json
is downloaded and deserialized. Resulting object is merged intodna.data.myConfig
property.new dna.MyClass
is instantiated and stored indna.myService
property- promise is resolved
Sometimes you will need to load Javascript class that has the name that conflicts with other class. Usually it is the case of supporting different versions of the same class.
In that case you can use prototype alias to export the prototype in different property. Example:
dna({'proto':'MyStuff','load':'/lib/my-stuff-v1.js'},{'proto':'MyStuff=MyStuff@2','load':'/lib/my-stuff-v2.js'});dna('MyStuff@2',newerCodeCallback);
In this case the classMyStuff
from the filemy-stuff-v1.js
will be exported asdna.MyStuff
while the same class from filemy-stuff-v2.js
will be exported asdna["MyStuff@2"]
.
You can use also multiple aliases:
dna({'proto':'MyStuff=MyStuff@2=webdevelopers.eu:MyStuff@2','load':'/lib/my-stuff-v2.js'});
in which caseMyStuff
frommy-stuff-v2.js
will be available as bothdna["MyStuff@2"]
anddna["webdevelopers.eu:MyStuff@2"]
but not asdna.MyStuff
.
You can execute your scripts in various ways. You can even register your ownCustom Evaluation Engines.
DNA comes with following evaluation engines.
Your script is expected to define variable matching the name specified in config'sproto
property that holds the prototype object representing your module.
Example:
dna({'proto':'MyModue','load':'/mymodule.js','eval':'dna'// default});
Contents of/mymodule.js
is expected to defineMyModule
variable holding the Object. For example:
functionMyModule(){}
Sometimes you need to asynchronously load other parts of the module and you cannot define the prototype right away during script evaluation.
For that thedeferred
engine is the right one as it expects you to pass the prototype object toPromise
when you are ready.
Example:
dna({'proto':'MyModue','load':'/mymodule.js''eval':'deferred'});
Contents of/mymodule.js
is expected to callfactory.resolve(...);
when your module is ready.
// variable `factory` is already populated with Deferred object you are expected to resolve/reject.doSomeAsyncInit.done(function(myProto){// myProto prototype is the outcome of your module.// It will be passed on to DNA to be registered in dna.MyModule property.factory.resolve(myProto);});
This Engine will allow you to include other extensive configurations on request. That way you can chain up .json configurations and modules that will be loaded on request or can be specified as dependencies for other modules.
dna({'id':'extensive:module','load':'#"/lot-of.json", "extensive:loader", function() { factory.resolve(); });','eval':'deferred'});// will load /lot-of.json with additional configuration// and initialize service dna['extensive:loader']dna('extensive:module',function(){// My extensive module is ready});
Note: for the trick with"load": "#"
seeCustom Downloader section.
Nowadays Javascript loader should download scripts asynchronously and out-of-order. DNA pushed it even further by making whole API fully out-of-order (O₃ API) to match your needs for worryless coding.
You can define callbacks before you load configurations. DNA will delay your callback's resolution until it gets enough information to resolve all dependencies.
// DNA is not loaded yet? Create surrogate object.vardna=dna||[];// Treat dna.push([ARGS]) as it were dna(ARGS)dna.push(['MyService',doSomething]);// Note - DNA is not loaded yet and just line before// you specified dependency on MyService that was not defined eitherdna.push({'proto':'MyService','load':['my1.js','my2.js']});
And when DNA is included everything falls in place automatically anddoSomething()
will get executed.
You can pass object with settings todna(SETTINGS)
method. Supported properties are
timeout
:Integer
number of milliseconds to wait before failing when trying to satisfy object requirements. Default: 5000rewrite
:object
seeCore Plugin Systemfactory
:object
seeCore Plugin Systemdownloader
:object
seeCore Plugin System
Exampe:
dna({'timeout':1000});
The main difference between asynchronous loaders is how loader
- interprets URLs
- downloads files
- evaluates scripts
Javascript DNA has core plugin system that allows you to define your own behavior for all of main components.
To register your plugins pass the plugin configuration object to dna:
dna({'rewrite':rewritePlugins,'downloader':downloaderPlugins,'factory':factoryPlugins});
To register your own URL rewritting callback use this syntax
dna({'rewrite':function(currentURI,originalURI,baseURI)|[function(currentURI,originalURI,baseURI), ...]});
Example:
if(server.development){dna({'rewrite':function(currentURI,originalURI,baseURI){returncurrentURI.replace(/\.min\.js$/,'.js');}});}
You can register multiple rewrite callbacks. They will be called in order of registration.
The resulting URI will be resolved to absolute URL if it is relative after all rewrite callbacks were applied.
You can also register your own URI downloader. That way you can download files not only from server but also from local storage, variables or other resources.
DNA has following native scheme downloaders
javascript
: able to exectue javascript URLs. Eg.'load': '#"Hello World!");'
css
: able to load CSS URLs. Eg.'load': 'css:./modules/my.css'
config
: able to load additional DNA configurations. Eg.'load': 'config:./modules/my.json'
remote
: able to load scripts from third-party domains. Eg.'load': 'remote:https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js'
*
: default downloader that uses standard$.AJAX
call.
The inbuiltjavascript
scheme hook allows you to embed javascripts into URLs.
dna({'id':'my-test','load':'#"Hello World!");'},'my-test'});
Inbuiltcss
sheme downloader allows you to embed CSS.
dna({'id':'my-test','load':'css:./my.css'},'my-test'});
Inbuiltconfig
sheme downloader allows you to load additional DNA configurations on demand.
dna({'id':'my-test','load':['config:my/dna.json','my/script.js'},'my-test'});
You cannot specify requirement that referre toConfiguration Object loaded fromload
section usingconfig
scheme. Error example:
{'id':'my-test','description':'ERROR: the requirement to load big-project.json will be never met.''require':'ClassInBigProject','load':['config:my/big-project.json','my/script.js']}
If you you want to specify requirement that yet-to-be-loaded from other JSON file you have to put both reference to that JSON fileand requirement from that file inrequire
statement. Correct example:
{'id':'my-test','require':['my/big-project.json','ClassInBigProject'],'load':'my/script.js'}
If the configuration object contains onlyid
property andrequire
property that contains URLs of other JSON configurationsthen such configuration can be overriden by other configuration withthe sameid
.
For example you can have one configuration file
[{'id':'my-test','require':'./other.json'}]
that includesother.json
file
[{'id':'my-test','proto':'MyTest','load':'./my-test.js'}]
and DNA will not complain about duplicateid
super-identifier andthe original configuration will be replaced with the one fromother.json
. That way you can split large JSON configurations intomultiple JSON files.
Inbuiltremote
sheme downloader allows you to load scripts from third-party domains.
dna({'id':'my-test','load':['load':'remote:https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js','my/script.js'},'my-test'});
You can register only one downloader for each URI scheme.
To register your own downloader use this syntax
dna({'downloader':{SCHEME:function(dfd,uri,config), ...}});
Your downloader is expected to calldfd.reject(ERROR)
ordfd.resolve(DATA_STRING)
after the download.
Example:
dna({'downloader':{'variable':function(dfd,uri,config){varcontents=myCachedContents[uri.replace('variable:','')];if(contents)dfd.resolve(contents);elsedfd.reject(newError('Cannot download URI "'+uri+'"!'));}}});dna({'proto':'Test','load':'variable:myTest'},'Test',callback);
Note: When your downloader returnsfalse
then default$.AJAX
downloader will be called instead. If you returnfalse
from your downloader then you are supposed not to resolvedfd
Deferred object.
You can specify your own function to execute downloaded scripts. That way you can bridge RequireJS or CommonJS or any other module format.
To specify execution handler use this syntax
dna({'factory':{EVAL:function(dfd,jString,protoName,config), ...}});
Your factory is expected to calldfd.reject(ERROR)
ordfd.resolve(FUNCTION)
after resolution.
Example:
dna({'factory':{'my-common-js':function(dfd,jString,protoName,config){varexports={};(function(exports){eval(jString);}(exports));dfd.resolve(exports[protoName]);// Return the exported object},'my-other-method':mySuperEvaluator}});dna({'proto':'MyModule','load':'/lib/my.js','eval':'my-common-js'});
Note: Thanks to Ozone API if you try to require a class that has unknowneval
type then the request will be queued until apropriateeval
type is defined. O₃ API allows you to define custom factories anytime without worrying if any code requiring custom factory was called before it has been even defined.
In fact this should allow also ECMA6 bridge.
// app.jsimportsomethingascalculatorfrom'calculator';console.log(calculator.sum(1,2));// => 3
where factory can search forimport
statement, do sub-call to DNA to resolve the found dependency and remove theimport
statement for compatibility with non-ECMA6 browsers... :-)
You can bundle multiple scripts into one XML or HTML file for optimized download.
Just create a document (e.g.my-bundle.html
) and put standardscript
tags withid
attributes in it.
<scriptid="myScript">function MyObject() {alert('Hello World!');}<script>
When specifyingload
property of the DNA Configuration object use the URL pointing to HTML file with hash part specifying the element ID.
{'proto':'MyObject','load':['/my-bundle.html#myScript','/my-unbundled.js']}
DNA will download themy-bundle.html
file only once (and reuse it later for other included scripts) and then it will extract and execute script with the attributeid="myScript"
.
It is good idea to make sure your web server allows browser-side caching of XML/HTML files.
For developement you can have emptyscript
tags linking to external javascripts:<script src="/libs/myscript.js"></script>
.DNA will figure out that the content ofscript
tag is missing and will use linked resource instead.
For production site you can populate the HTML with embeded scripts or use the web serverPageSpeed Module or other tools to do it automatically for you.
dna.core.configs
exposes the list of all known configurations.
If your configuration contains custom data you may search for them indna.core.configs
.
Example: Some configurations have propertystartup
set to trueand we want to run all configs marked that way.
Your startup service in yourdna.json
config may look like this:
{"service":"MyBootstrap","startup":true,"load":"mybootstrap.js"}
// Find all existing services that should be ran on start updna.core.configs.filter((config)=>config.startup)// filter only startup=true props.forEach((config)=>dna(config.service));// start all services
Thanks to the nature of DNA configs may be loaded later after the codeabove was executed. To watch for newly added configs you may want toleverage [Notifying of New Configurations](#Notifying of New Configurations):
$(window).on("dna:config:new",function(ev,config){if(config.service&&config.startup){dna(config.service);}});
Thedna:config:new
event is fired onwindow
object on each newslyadded configuration object. To listen for newly added configuartions use this code:
$('window').on("dna:config:new",function(message){alert('New DNA config added: '+JSON.stringify(message.data));});
Load jQuery plugins
dna('/dna-configs/my.json','jquery:my');
Contents ofmy.json
(note theCSS scheme trick)
[{"id":"jquery:my","load":["css:js/jquery.my.css","js/jquery.my.js"]}]
Mixed confugration using JSON file and inline Configuration Objects + requiring servicedna.svc2
and prototypedna.Svc1
:
dna('Svc1','svc2','/configs/svcs.json',{'service':'svc2','proto':'Svc2','load':['/js/base.js','/js/svc2.js']}).done(run).fail(ups);
Out-of-order calls (O₃ API): first requiredna.Svc1
anddna.svc2
to run your callbackrun
and then later on load required configurations.
dna(['Svc1','svc2'],run);dna('/configs/svcs.json');dna({'service':'svc2','proto':'Svc2','load':['/js/base.js','/js/svc2.js']});
Making DNA calls beforedna.js
gets loaded using asynchronousscript
tag.
<script>vardna=dna||[];dna.push(function(){alert('DNA just loaded!');});dna.push(['svc',function(svc){alert('Service `dna.svc` is ready!');}]);</script>...<scriptasyncsrc="/dna.js"></script>
Contents ofindex.html
:
<script>vardna=dna||[];dna.push(function(){// on DNA loaddna('/app/config.json','myApp',function(){// load and start my appdna.myApp.start();});});</script>...<scriptsrc="/dna.js"async></script>
Contents of/app/config.json
(relative paths are resolved relatively to JSON's file directory/app/
):
[{'service':'myApp','proto':'MyApplication','require':'app:base','load':'./my.js'},{'id':'app:base','load':['./base/jquery.js','/lib/bootstrap.js'],'context':'window'}]
Contents of/app/my.js
:
functionMyApplication(){this.version='0.1';}MyApplication.prototype.start=function(){alert('Hello world!');}
If you write piece of code for global distribution then make sure you create configuration with globally (worldwide) unique ids so programmers using your code can integrate it without changes to your configs.
Good idea is to prefix your super-identifiers with your domain name.
Example of yourconfig.json
file:
[{'proto':'Example=example.com:Example','require':['example.com:Main','example.com:service'],'load':'./example.js'},{'service':'example.com:service','proto':'ServiceProto=example.com:ServiceProto','require':'example.com:Main','load':'./service.js'},{'proto':'Main=example.com:Main','load':'./main.js'}]
Code in the example will result in exports intodna["example.com:..."]
properties.
See more inPrototype Aliases section.
There are many ways how to leverage the strength of DNA in your project.
You can make DNA calls even before DNA was loaded.
Create simple[]
array if DNA is not loaded yet and call thedna.push([arguments])
method on it as it wasdna(arguments)
method.
<script>vardna=dna||[];dna.push(['service1',function(svc1){...}]);dna.push(callbackOnDNAStart);</script>...<scriptsrc=".../dna.js"async="async"></script>
Note: Multiple arguments must be passed todna.push()
as an array.
There is one limitation though, thedna.push()
method does not return thePromise object so you must pass your on-success callbacks as arguments.
Store your configurations in JSON file and load it, don't forget thatdna()
always returns the jQueryPromise object.
dna('/my-defs.json','MyObject1','MyObject2',myCallback).done(myOtherCallback).done(myOtherOtherCallback,oneMoreCallback).fail(myWTF);
You can usedna()
to load any script that was not directly written for DNA.
dna({'id':'jquery','load':'/libs/jquery.min.js','context':'window'},{'id':'jquery:iPop','require':'jquery','load':'/libs/jquery.ipop.js','context':'window'});dna('jquery:iPop',callback);
Most of older scripts can be specified usingid
attribute and executed usingcontext
typewindow
. To support newer scripts (like AMD scripts) usecustom factories that you can tailor to fit any framework and/or your special needs.
Sometimes selected scripts need to share the variables. Polluting globalwindow
scope with variables is not the best solution.
With DNA you can use the experimental named contexts. Scripts sharing the same name of the context will havethis
and variablecontext
set to their own shared Object.
dna({'id':'test:1','load':'#"var 1"; console.log("Script 1", context.myVar1, context.myVar2);','context':'my-private'},{'id':'test:2','load':'#"var 2"; console.log("Script 2", context.myVar1, context.myVar2);','context':'my-private'},'test:1','test:2');
If named context is not specified then withdna
eval mode each configuration has its own private Object set as context automatically.
Watch the Javascript Console.
- Document $(window) events 'dna:fail', 'dna:done', 'dna:always'
About
Simple to use asynchronous resource (javascript, CSS, ...) loader and dependency resolver for Javascript apps. Extensible plugin system. Support for bundled javascript assets and more.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Uh oh!
There was an error while loading.Please reload this page.