refactor(component-mapper): introduce a proxy for module.exports
MODIFY
packages/compartment-mapper/src/parse-cjs.js
packages/compartment-mapper/src/parse-cjs.js
@@ -21,21 +21,36 @@ export const parseCjs = async (
);
/**
- * @param {Object} moduleExports
+ * @param {Object} moduleEnvironmentRecord
* @param {Compartment} compartment
* @param {Record<string, string>} resolvedImports
*/
- const execute = (moduleExports, compartment, resolvedImports) => {
+ const execute = (moduleEnvironmentRecord, compartment, resolvedImports) => {
const functor = compartment.evaluate(
`(function (require, exports, module, __filename, __dirname) { ${source} //*/\n})\n//# sourceURL=${location}`,
);
- let exportsReferenceCopy = moduleExports;
+
+ const originalExports = new Proxy(
+ Object.create(compartment.globalThis.Object.prototype),
+ {
+ get(target, prop) {
+ return moduleEnvironmentRecord[prop] || target[prop]; // this makes things like exports.hasOwnProperty() work.
+ },
+ set(target, prop, value) {
+ moduleEnvironmentRecord[prop] = value;
+ return true;
+ },
+ },
+ );
+
+ let finalExports = originalExports;
+
const module = freeze({
get exports() {
- return exportsReferenceCopy;
+ return finalExports;
},
set exports(value) {
- exportsReferenceCopy = value;
+ finalExports = value;
},
});
@@ -53,13 +68,13 @@ export const parseCjs = async (
functor(
require,
- exportsReferenceCopy,
+ finalExports,
module,
location, // __filename
new URL('./', location).toString(), // __dirname
);
- if (exportsReferenceCopy !== moduleExports) {
- moduleExports.default = exportsReferenceCopy;
+ if (finalExports !== originalExports) {
+ moduleEnvironmentRecord.default = finalExports;
}
};

ADD
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/app/cycle1.js
+exports.useCycle = function() {
+ return 1;
+};
+
+var cycle2 = require('./cycle2');
+
+exports.isOk = !!cycle2.isOk;

ADD
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/app/cycle2.js
+const cycle1 = require('./cycle1');
+
+exports.isOk = cycle1.useCycle();

MODIFY
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/app/index.js
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/app/index.js
@@ -2,6 +2,8 @@ const callself = require('callself');
const interops = require('interops');
const moduleinterops = require('./moduleinterops');
const stableOrderJson = require('./assertion-generators/json.cjs');
+const cycle1 = require('./cycle1');
+const defineprop = require('defineprop');
// Generated by running v16.13.2: node assertion-generators/expected.cjs
// I chose this behavior because:
@@ -23,6 +25,20 @@ function assertInteropNameCollisions({ moduleReference, expect, name }) {
}
module.exports.assertions = {
+ packageWithDefineProperty() {
+ if (!defineprop.isOk) {
+ throw Error(
+ 'Expected exports created by defineProperty to be available',
+ );
+ }
+ },
+ moduleWithCycle() {
+ if (!cycle1.isOk) {
+ throw Error(
+ 'Modules depending on each other in a cycle failed to get references from exports',
+ );
+ }
+ },
packageWithDefaultField() {
assertInteropNameCollisions({
name: 'interops',

MODIFY
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/app/package.json
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/app/package.json
"main": "./main.js",
"dependencies": {
"callself": "^1.0.0",
+ "defineprop": "^1.0.0",
"interops": "^1.0.0"
}
}

ADD
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/defineprop/main.js
+// fragments of logic adapted from ethereumjs-util@7.1.0
+'use strict';
+// prettier-ignore
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+// prettier-ignore
+var __exportStar = (this && this.__exportStar) || function(m, exports) {
+ for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p);
+};
+Object.defineProperty(exports, '__esModule', { value: true });
+/// <reference path="./@types/ethjs-util/index.ts"/>
+
+const stuff = { isOk: 1 };
+__exportStar(stuff, exports);

ADD
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/defineprop/package.json
+{
+ "name": "defineprop",
+ "exports": "./main.js"
+}

MODIFY
packages/compartment-mapper/test/test-cjs-compat.js
packages/compartment-mapper/test/test-cjs-compat.js
@@ -9,6 +9,22 @@ import { makeReadPowers } from '../src/node-powers.js';
const readPowers = makeReadPowers({ fs, url });
const { read } = readPowers;
+test('CommonJS modules with cyclic dependency', async t => {
+ t.plan(1);
+
+ const fixture = new URL(
+ 'fixtures-cjs-compat/node_modules/app/index.js',
+ import.meta.url,
+ ).toString();
+
+ const application = await loadLocation(read, fixture);
+ const {
+ namespace: { assertions },
+ } = await application.import({});
+ assertions.moduleWithCycle();
+ t.pass();
+});
+
test('CommonJS referencing exports', async t => {
t.plan(1);

refactor(component-mapper): introduce a proxy for module.exports (#1073)