Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(endo): Add CommonJS and JSON module support #397

Merged
merged 16 commits into from
Aug 24, 2020
Merged

Conversation

kriskowal
Copy link
Member

@kriskowal kriskowal commented Jul 31, 2020

Achieves parity with Node.js behavior regarding the "type": "module" declaration in package.json, determining whether .js files are CommonJS or ECMAScript modules. Also treats the "module" declaration as a hint that the denoted module is an ESM, regardless of its extension.

The emulation of Node.js behavior is not exact.

  • In Node.js, ECMAScript modules can use require to import CommonJS modules, but cannot use import from CommonJS. In Endo.js, an ESM can only use import and a CJS module can only use require, but both can be used to import any supported module including ECMAScript modules, CommonJS modules, and JSON modules.
  • In Node.js, a CommonJS script cannot use require to import a CommonJS. module. In Endo.js, they can.
  • The Endo.js convention for ESM importing CJS is that require will return the default export if it exists and assigning to module.exports is equivalent to assigning to exports.default. This may break existing CommonJS code in the rare case that has an export named default. Consequently, ESM and CJS destructuring imports are the same.
  • This works because Endo.js uses the same heuristics as build-tools for CommonJS imports. The require calls must be exclusively static, meaning the argument must be a string and all imports are unconditional.
  • Of course, Endo.js only works for modules within packages. Imports cannot reach arbitrarily outward into the filesystem.
  • Node.js ESM cannot use import to load a JSON module. Endo.js can import JSON into any module. The module formats are orthogonal.

@kriskowal kriskowal requested a review from warner July 31, 2020 00:33
@kriskowal
Copy link
Member Author

cc @kumavis This came together faster than expected.

@kriskowal
Copy link
Member Author

Replaced the whacky regex with a proper Babel visitor, so static analysis for require(id) calls is now as accurate as it will likely get.

@warner
Copy link
Contributor

warner commented Aug 5, 2020

Could you rebase this when you get a chance? The diff currently shows all the endo-main changes too, which should go away when the branch is moved to sit properly on top of the updated target.

@kriskowal
Copy link
Member Author

Could you rebase this when you get a chance? The diff currently shows all the endo-main changes too, which should go away when the branch is moved to sit properly on top of the updated target.

Rebased this stack.

@kriskowal kriskowal mentioned this pull request Aug 6, 2020
36 tasks
@kriskowal kriskowal force-pushed the kris/endo-cjs branch 2 times, most recently from 9d1453c to 8e6f345 Compare August 10, 2020 22:44
@kriskowal kriskowal force-pushed the kris/endo-cjs branch 2 times, most recently from 1e1a19c to e4ef2b7 Compare August 12, 2020 00:35
Base automatically changed from kris/endo-main to master August 22, 2020 19:12
@kriskowal kriskowal force-pushed the kris/endo-cjs branch 2 times, most recently from 165bf77 to 4ff24fb Compare August 22, 2020 20:45
Copy link
Contributor

@warner warner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor changes. looks good!

test("parse unique require calls", t => {
t.plan(1);
const code = `
require("b"); // sorted later
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the code were failing to find inner-scope'd require, would this second occurrence of require("b") make this test fail to find the bug? I.e does this occurrence nullify the negative predictive value of the test?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, maybe this test should exercise a space betwen require and ( , and perhaps a newline, if those are things that Node will accept

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eh, probably not worth it, we are using a proper parser after all. (back in my day, we made do with regexps, and I would have been worried about something like this. Six feet of snow, uphill both ways, yadda yadda)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s a commit with the regex version in this very stack, borrowed from my back-in-the-day implementation ❤️

@@ -0,0 +1,6 @@
/* global lockdown */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file doesn't appear to be used (yet)?

* the module execution phase.
* For example, `require(path)` will not be discovered by this parser, the
* module will successfully load, but will likely be unable to synchronously
* require the module with the given path.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in Jetpack, our recommendation for this was to combine the two effects:

if (false) { require('thing1'); }
if (false) { require('thing2'); }
...
const needed = condition ? 'thing1' : 'thing2';
require(needed);

.. but really, if you're reaching for something like that, there's probably something deeper going on, and you're going to get into trouble anyways.

}

const imports = parseRequires(source, location);
const execute = (exports, compartment, resolvedImports) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, why is exports passed into execute() when it seems the only properties on it come from the module being loaded? I would have expected execute() to return exports instead. Is this some quirk of the new module loader specification?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CommonJS has always supported cyclic dependencies in this fashion. Two modules can mutually require as long as they lazy-bind properties of the exports returned by require, and as long as they use exports.property = assignment instead of module.exports = replacement. The module.exports property was not part of the original CommonJS, although it has become dominant usage in Node.js. The module object originally existed in CommonJS to fulfill the same function as import.meta.

};

const parserForLanguage = {
mjs: parseMjs,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts about using "esm" for the name of the language, rather than "mjs"? I was puzzled for a while until I realized this was not an extension name.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you noted later, this becomes coherent with the "parsers" directive in package.json that we need in order to break the log-jab between Node.js and -r ESM’s interpretation of the "type": "module" directive. I felt that translating between "esm" in the domain and "mjs" in the range for just this one format may have been more confusing.

require("a");

function b() {
require("b"); // found despite inner scope
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs a different name (maybe d) to test inner scopes without interference from the same-named require('b') on line 9. If the parser had a bug in which inner scopes were ignored, this test would yield a false positive.

require("a"); // de-duplicated

require("id\\""); // found, despite inner quote
`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might consider a test that require('e', otherarg) is ignored, since that's known behavior of the parser. OTOH I'd understand if you wanted to omit it, since ignoring multi-arg require is not really part of the spec.

@@ -70,6 +73,21 @@ const findPackage = async (readDescriptor, directory, name) => {
}
};

const commonParsers = { js: "cjs", cjs: "cjs", mjs: "mjs", json: "json" };
const moduleParsers = { js: "mjs", mjs: "mjs", cjs: "cjs", json: "json" };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the sake of visual comparison, these should both have their keys in the same order, rather than swapping cjs/mjs between the two

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nevermind, I see this issue goes away in another couple of PRs

@kriskowal kriskowal merged commit fe927df into master Aug 24, 2020
@kriskowal kriskowal deleted the kris/endo-cjs branch August 24, 2020 19:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Compartment support for CommonJS
2 participants