eclipse-archived / ceylon

The Ceylon compiler, language module, and command line tools
http://ceylon-lang.org
Apache License 2.0
398 stars 62 forks source link

Enhanced support for importing JavaScript exports #7006

Open lucono opened 7 years ago

lucono commented 7 years ago

There are many JavaScript libraries where the exported item is exported as the "exports" property of "module", ie:

// some-lib.js
module.exports = myExportObject;

For these, there should be a way in Ceylon to get a reference to the entire exports object (basically, get a handle on "module.exports" object) using Ceylon's import statement rather than require-ing the module in a dynamic block.

Basically, if I had a library that does this:

// my-module.js

var myData = {
    a: "one",
    b: "two",
    c: "three"
};
module.exports = myData;

Then, there should be a way to obtain myData in Ceylon, using the import statement, something like this:

// ceylon

import my.module { myModule }

dynamic {
    print(JSON.stringify(myModule ));  // { "a":"one", "b":"two", "c":"three" }
}

Rather than having to do this:

// ceylon

dynamic {
    dynamic myData = require("my-module");
    print(JSON.stringify(myData));  // { "a":"one", "b":"two", "c":"three" }
}

Ceylon already has some special handling for "Non-standard modules" described here, which accounts for cases where the main module export is a function that is directly exported over module.exports, but doesn't account for the case where the main export is an object that is directly exported over module.exports.

In its current special handling, Ceylon will provide access to the entire exports object with Ceylon's import statement only if it finds that it's a function, but I don't see any reason why this scheme couldn't be generalized for all situations - that the module name always imports whatever is the value of module.exports.

This would allow it to work for both functions and objects (or anything else) exported over module.exports, while allowing for backward compatibility with applications already using the module name to import functions exported this way in Ceylon's current special handling of the case.

gavinking commented 7 years ago

@lucono does the following not work:

import some.other.modul { exports { myData } }

If not, then yeah, I think we should make it work.

lucono commented 7 years ago

@gavinking No, that doesn't work.

gavinking commented 7 years ago

Well what is the error?

lucono commented 7 years ago

There's no error, just that it's undefined.

It seems Ceylon doesn't make the entire CommonJS module.exports value available through the Ceylon import statement. Instead, if module.exports is an object, then Ceylon provides access (with Ceylon import statement) to only the properties under that object, with the additional special case handling described here - https://github.com/ceylon/ceylon/wiki/NPM-and-Ceylon-JS#non-standard-modules.

gavinking commented 7 years ago

@chochos WDYT?

chochos commented 7 years ago

Didn't we already have the special require that checked if the returned object was really just a function or something and if so, put it inside an object so that it was a standard thing?

chochos commented 7 years ago

I remember bumping into this before, but I'm not sure what we did about it, if anything.

lucono commented 7 years ago

@chochos I think what you ended up doing is what's described at the bottom of the page here, under the section Non-standard Modules.

Basically, some special handling was implemented for when the returned "object" is really just a function, but that special handling is also relevant for certain cases where the returned object is indeed a regular object.

So, Ceylon JS could benefit from a generalization of that handling, as I described earlier above, where the (entire) returned value from the export is made accessible under a special name in the package import. In the special handling you implemented for when the export is a function, that special name is the name of the module, but a set name such as exports could also be used. This of course would only be relevant to JavaScript/npm modules imported in a Ceylon module.

CommonJS JavaScript module:

// my-module.js

var someData = {
    a: "one",
    b: "two",
    c: "three"
};
module.exports = someData;  // Note that not: module.exports.someData = someData;

And in Ceylon:

// uses the JS module's name "myModule" to import the entire export value
import my.module { myModule, a, b, c }

dynamic {
    print(JSON.stringify(myModule));  // { "a":"one", "b":"two", "c":"three" }
    print(a);  // "one"
    print(b);  // "two"
    print(c);  // "three"
}

OR:

// uses special name "exports" to import the entire export value
import my.module { myModule = exports, a, b, c }

dynamic {
    print(JSON.stringify(myModule));  // { "a":"one", "b":"two", "c":"three" }
    print(a);  // "one"
    print(b);  // "two"
    print(c);  // "three"
}

I imagine the choice of name to use for the import of the entire export value should be one that has the least likely chance of collision given common practices in real life modules. It could also be something that could not possibly be a field name in most JavaScript modules, such as the JS keyword export (except of course if doing something like module['export'] = someData), but not a Ceylon keyword, allowing something like this:

// uses special name "export", a JS keyword but not a Ceylon keyword
import my.module { myModule = export, a, b, c }

dynamic {
    print(JSON.stringify(myModule));  // { "a":"one", "b":"two", "c":"three" }
    print(a);  // "one"
    print(b);  // "two"
    print(c);  // "three"
}

In either case, the examples above use import aliasing to alias it to a more meaningful name in the Ceylon file.