duktape
duktape copied to clipboard
ES2015 Module
This is probably an incremental update at best. This is a simple support of ES6 module api from within scripts.
Overall Feature Tasks:
- [ ] Implement import keyword
- [ ] Implement Export Keyword.
- [ ] Implement C Module API.
- [ ] Implement various Tests.
Example User Code For above Tasks: example Library 1 (in JavaScript):
// lib/math.js
// Create a few items:
var pi = 3.141593
var half_pi = 1.5707965
var foo = "I am really foo disguised as bar"
// a few valid examples of exporting..
export foo as bar; // This is also valid.
export {half_pi, pi};
export {half_pi as pi_over_2, pi as pi_times_one};
export function sum (x, y) { return x + y }
export var two_pi = 6.283186
example Library 2 (in JavaScript):
// lib/altmath.js
// This simply re-routes the modules in lib/math.js.
export {pi as i_like_pi, sum as i_can_add } from "lib/math";
// pi, and sum are not visible to scripts importing this lib.
// i_like_pi = 3.141593
// i_can_add = function (x,y) { return x + y }
Example Application 1:
// someApp.js
import * as math from "lib/math"
console.log("2p = " + math.sum(math.pi, math.pi))
Example Application 2:
// otherApp.js
import { sum, pi } from "lib/math"
console.log("2p = " + sum(pi, pi))
Example Application 3:
// This creates the myMathPi object using the object pi from lib/math.
import pi as myMathPi from "lib/math"
console.log("myMathPi = " + myMathPi);
Example Application 4:
import { bar } as myMath from "lib/math"
console.log("myMath.bar is: " + myMath.bar );
try {
console.log("1 + 2 = " + myMath.sum(1,2));
console.log("myMath.pi = " + myMath.pi);
} catch(e) {
// runtime error is expected. Sum was not imported.
print("Error: " + e)
}
Edit: I forgot to add tasks. Edit 2: Added a few more examples.
For internals it'd be great if the same module loader could serve both CommonJS and the ES6 modules. Not sure what the impact is here yet.
Yea, This could be considered a huge update. Also, For all intensive purposes Modules could be viewed as large importable objects. Also, as far as C/C++ Registration of modules, I can see it done using the Following...
Example C Module:
duk_idx_t module_idx;
duk_idx_t obj_idx;
/* Create new Module */
module_idx = duk_push_module(ctx);
/* Add Custom Object to Module */
obj_idx = duk_push_object(ctx);
duk_push_number(ctx, 3.141593);
duk_put_prop_string(ctx, obj_idx, "pi");
/* object is now: { "pi": 3.141593 } */
duk_put_prop_string(ctx, module_idx, "Constants");
/* Name Said Module */
duk_put_prop_string(ctx, -1, "MyMath");
/* Module Now has the Object constants. */
/* MyMath.Constants.pi = 3.141593 */
duk_pop(ctx); /* pop Module */
Example App using Module:
import * as MyMath from "MyMath";
console.log("Pi = :" + MyMath.Constants.pi);
I thought of something that may be useful, It would probably be a good idea to fix issue #284 before implementing this request. The main reasoning is that in terms of how duktap handles objects, each module itself is it's own global object, and the items that can be found from within said object must be flagged with export first. This may be useful since the export flags could be used to flag properties in a way where it is only accessible from c/c++ only or from within the current global object only.
A little bit more examination shows that the current module system can probably be reused to power the ECMAScript 2015 modules... This is just covering what most users will be using in the first place.
- [ ] Implement export, import and from Keywords This controls most of the module syntax.
- [ ] Implement as keyword.
This renames elements from a module.
Minor Comparison of CommonJS modules to ECMAScript 2015 Modules.
Module exports
Basic Variable and function exporting.
CommonJs:
// CommonJSModuleA - example 1 for CommonJS Modules
var half_pi = 1.5707965
// Export a few variables.
exports.pi = 3.141593
exports.half_pi = half_pi;
exports.myFunc = function() {return "Hello from CommonJSModuleA.";}
ECMAScript 2015:
// MyModuleA - example 1 for ECMAScript 2015 Modules
var half_pi = 1.5707965
export var pi = 3.141593
export half_pi
export function myFunc() {return "Hello from CommonJSModuleA.";}
rename local variables on export:
CommonJS:
// CommonJSModuleB - example 2 for CommonJS Modules
var foo = "I am really foo disguised as bar"
function mySum (x, y) { return x + y }
exports.bar = foo
exports.sum = mySum;
ECMAScript 2015:
// MyModuleB - example 2 for ECMAScript 2015 Modules
var foo = "I am really foo disguised as bar"
function mySum (x, y) { return x + y }
export foo as bar;
export mySum as sum;
export items from another module:
CommonJS:
// CommonJSModuleB - example 3 for CommonJS Modules
var otherModule = require("CommonJSModuleB")
exports.bar = otherModule.bar
exports.sum = otherModule.sum;
ECMAScript 2015:
// MyModuleC - example 3 for ECMAScript 2015 Modules
export {bar, sum} from "MyModuleB";
Module Importing
Import module into the global space of a script:
CommonJS:
// TODO: Confirm this...
require('CommonJSModuleA');
// myFunc is from CommonJSModuleA.
print(myFunc())
ECMAScript 2015:
import * from 'MyModuleA';
print(myFunc())
Import module into a script as an object:
CommonJS:
var myModule = require('CommonJSModuleA');
// myFunc is from CommonJSModuleA.
print(myModule.myFunc())
ECMAScript 2015:
import * as myModule from 'MyModuleA';
print(myModule.myFunc())
import only a single item from a Module into Global Space:
CommonJS:
// TODO: Verify that this is correct.
var myModule = require('CommonJSModuleA');
myFunc = myModule.myFunc;
delete myModule;
myModule = NULL;
// myFunc is from CommonJSModuleA.
print(myFunc())
ECMAScript 2015:
import myFunc from 'MyModuleA';
print(myFunc())
import a group of items from a Module into global space.
CommonJS:
// TODO: Verify that this is correct.
var myModule = require('CommonJSModuleA');
myFunc = myModule.myFunc;
pi = myModule.pi;
delete myModule;
myModule = NULL;
// myFunc is from CommonJSModuleA.
print(myFunc())
print(pi)
ECMAScript 2015:
import {myFunc, pi} from 'MyModuleA';
print(myFunc())
print(pi)
import a group of items from a Module into an object.
CommonJS:
// TODO: Verify that this is correct.
var myModuleSrc = require('CommonJSModuleA');
myModule.myFunc = myModuleSrc.myFunc;
myModule.pi = myModuleSrc.pi;
delete myModuleSrc;
myModuleSrc = NULL;
print(myModule.myFunc())
print(myModule.pi)
ECMAScript 2015:
import {myFunc, pi} as myModule from 'MyModuleA';
print(myModule.myFunc())
print(myModule.pi)
import a group of items from a Module into an object with renaming.
CommonJS:
// TODO: Verify that this is correct.
var myModuleSrc = require('CommonJSModuleA');
myModule.Hello = myModuleSrc.myFunc;
myModule.iLikePi = myModuleSrc.pi;
delete myModuleSrc;
myModuleSrc = NULL;
print(myModule.Hello())
print(myModule.iLikePi)
ECMAScript 2015:
import {myFunc as Hello, pi as iLikePi} as myModule from 'MyModuleA';
print(myModule.Hello())
print(myModule.iLikePi)
I went ahead and looked at what is needed to implement the basics for each statement.
Modifications to Duktape source
NOTE: These modifications will not generate working code, but provide a basic flow for how the tokens could be handled in duktape...
tokens to add:
- 'as' token
- 'from' token
- 'default' token.
lexar for export token
This can be taken care of in a fairly straight forward fashion.
DUK_LOCAL void duk__parse_export_stmt(duk_compiler_ctx *comp_ctx, duk_ivalue *res) {
/* this will take two passes to complete. */
/*
* first pass: find the from statement, and import module mentioned in from.
*
*/
/* This is an example of how the second pass will probably work */
duk__advance(comp_ctx); /* eat 'export' */
switch (comp_ctx->curr_token.t) {
case DUK_TOK_MUL: { /* handle '*' */
duk__advance(comp_ctx); /* eat '*' */
/* This exports all properties */
if (comp_ctx->curr_token.t == DUK_TOK_FROM) {
duk__advance(comp_ctx); /* eat 'from' */
/* re-use duk__bi_global_resolve_module_id */
/* this is an implied import */
/* throw an error if import fails */
} else {
/* This cannot happen*/
}
break;
}
case DUK_TOK_DEFAULT: { /* handle 'default' */
/* Handle Function */
duk__advance(comp_ctx); /* eat 'default' */
if (comp_ctx->curr_token.t == DUK_TOK_FUNCTION) {
/* Handle Function */
/* if function does not have a name, local name is defined as *default* */
break;
} else {
/* this should be some sort of constant or variable */
/* if it is a undefined variable, the local variable name is *default* */
}
/* add to export table as default */
break;
}
case DUK_TOK_FUNCTION: { /* handle 'function' */
/* Handle function*/
duk__advance(comp_ctx); /* eat 'function' */
break;
}
case DUK_TOK_VAR: { /* handle 'var' */
/* Handle variable */
duk__advance(comp_ctx); /* eat 'var' */
break;
}
case DUK_TOK_LCURLY: { /* handle '{' list of arguments */
duk__advance(comp_ctx); /* eat '{' */
/* import module specified by 'from' statement.*/
/* begin variable look up */
/* current token is variable to look up */
if (comp_ctx->curr_token.t == DUK_TOK_AS) {
duk__advance(comp_ctx); /* eat 'as' */
/* place variable in exports as next token. */
}
/* add variable to exports list */
if (comp_ctx->curr_token.t == DUK_TOK_COMMA ) {
duk__advance(comp_ctx); /* eat ',' */
/* repeat variable look up */
}
if (comp_ctx->curr_token.t == DUK_TOK_RCURLY ) {
duk__advance(comp_ctx); /* eat '}' */
/* end variable look up */
}
if (comp_ctx->curr_token.t == DUK_TOK_FROM) {
duk__advance(comp_ctx); /* eat 'from' */
/* load target script, and then re-route exports list */
/* generate errors if module load fails */
} else {
/* add export list from local module. */
}
break;
}
default:
{
/* import module specified by 'from' statement.*/
/* get variable name */
if (comp_ctx->curr_token.t == DUK_TOK_AS) {
duk__advance(comp_ctx); /* eat 'as' */
/* get export name if different */
}
/* place into export table */
}
}
}
Basic lexar for the import token:
DUK_LOCAL void duk__parse_import_stmt(duk_compiler_ctx *comp_ctx, duk_ivalue *res) {
/* This is an example of how the second pass will probably work */
duk__advance(comp_ctx); /* eat 'import' */
switch (comp_ctx->curr_token.t) {
case DUK_TOK_MUL: { /* handle '*' */
duk__advance(comp_ctx); /* eat '*' */
/* This exports all properties */
if (comp_ctx->curr_token.t == DUK_TOK_AS) {
duk__advance(comp_ctx); /* eat 'as' */
/* this will specifiy the output object to place imported symbols into */
} else {
/* place exports into global space. */
}
if (comp_ctx->curr_token.t == DUK_TOK_FROM) {
duk__advance(comp_ctx); /* eat 'from' */
/* this is required */
/* load module exports into specified object */
}
break;
}
case DUK_TOK_LCURLY: { /* handle '{' list of arguments */
duk__advance(comp_ctx); /* eat '{' */
/* generate list of variables to import */
if (comp_ctx->curr_token.t == DUK_TOK_AS) {
/* A as B -> place variable A into target as B */
duk__advance(comp_ctx); /* eat 'as' */
}
if (comp_ctx->curr_token.t == DUK_TOK_COMMA ) {
duk__advance(comp_ctx); /* eat ',' */
/* Repeat List look up.
}
if (comp_ctx->curr_token.t == DUK_TOK_RCURLY ) {
duk__advance(comp_ctx); /* eat '}' */
/* end variable look up */
}
/* actually load module */
if (comp_ctx->curr_token.t == DUK_TOK_AS) {
duk__advance(comp_ctx); /* eat 'as' */
/* this will specifiy the output object to place imported symbol into */
} else {
/* place exports into global space. */
}
if (comp_ctx->curr_token.t == DUK_TOK_FROM) {
duk__advance(comp_ctx); /* eat 'from' */
/* this is required */
/* load target script, and then re-route exports list */
/* generate errors if module load fails */
} else {
/* throw an error. No import specified */
}
break;
}
default:
{
/* import module specified by 'from' statement.*/
/* get variable name */
if (comp_ctx->curr_token.t == DUK_TOK_AS) {
duk__advance(comp_ctx); /* eat 'as' */
/* get export name if different */
}
if (comp_ctx->curr_token.t == DUK_TOK_FROM) {
duk__advance(comp_ctx); /* eat 'from' */
/* load target script, and then re-route export */
}
}
}
}
Having started taking advantage of ES6 modules in minisphere through Babel, I have to say I really like the import/export syntax. It's very direct, as compared to the more passive voice inherent to the CommonJS idioms (e.g., "import X from Y" vs. "require module Y to use X"; "export X" vs. "this module exports X").
As for module loading, Ecmascript itself doesn't mandate any particular semantics for that, but WHATWG has been working on a standard: https://github.com/whatwg/loader
Is there any progress on this?
Not much, it is pending on a revised parser with more state.
@svaarala is this maybe going to be possible to be implemented in the 3.0 or later update? Really wanting to ditch babel in my project.