Skip to content

ES6 module loader compatibility #256

Closed
@RReverser

Description

@RReverser

Current napi_addon_register_func callback is quite obviously designed around CommonJS (for understandable reasons), but given that N-API is a new API, it might be easy enough to provide something that will be module-system-agnostic and work well with CommonJS while also being aligned better with ES6 import statements that are upcoming in Node.js.

One of the main incompatibilities from aligning current CommonJS ecosystem with ES6 comes from the fact that in CommonJS you can either export arbitrary named declarations, or you can completely redefine module.exports object with any JS value.

ES6 module system, on the other hand, does not allow such conflicts and presents all the exports on a single object with explicit names (or default as a key), so there are arguments about how to import module.exports case - should it be imported as default one and then effectively each CommonJS module can be used only as import fs from "fs" and not import { readFile } from "fs", or should it do some magic and destructure itself into ES6 names exports as well.

To avoid similar incompatibilies, my suggestion is to make loader system opaque to the registration callback and either:

1. Allow only named exports by having something like:

typedef struct {
  const char* name; // NULL for default export
  napi_value value;
} napi_export_descriptor;

NAPI_EXTERN napi_status napi_export(napi_env env, size_t export_count, const napi_export_descriptor* descriptors);

Which would do equivalent of exports.name = value / exports.default = value in CommonJS and export const name = value / export default value in ES6 module system.

This might be incompatible with some existing modules though that effectively used module.exports = ... and such change would require them updating require(...) part too.

2. Allow only "default" export. Effectively this means changing a callback to:

typedef void (*napi_addon_register_func)(napi_env env, napi_value *result);

For modules that just defined properties on given exports empty object, the only difference would be that they will have to call napi_create_object theirselves and return it in the result.

For modules that used to override module.exports, this only simplifies things as they don't need to use property modification APIs to change "exports" property on a given module and can just return the value directly.

For CommonJS users nothing will change and require(...) will work exactly like before.

For ES6, this will allow to import such modules with import nativeModule from "..." and using its properties - this won't allow having named imports unfortunately, but that's a minor inconvenience, and it will allow module loader to load native module without having to simulate CommonJS' module and exports wrapper objects.


Personally, I'm leaning towards option #2 as that's a pretty simple change that aligns well with both CommonJS and ES6 loaders without breaking compatibility with existing native modules in the ecosystem, but would love to hear your thoughts.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions