refactor(compartment-mapper): rebuild cjs exports handling with the assumption module.exports is the default export
ADD
packages/compartment-mapper/src/parse-cjs-shared-export-wrapper.js
+// @ts-check
+const { apply } = Reflect;
+const { freeze, keys, create, hasOwnProperty } = Object;
+
+/**
+ * @param {Object} object
+ * @param {string} key
+ * @returns {boolean}
+ */
+const has = (object, key) => apply(hasOwnProperty, object, [key]);
+
+/**
+ * ModuleEnvironmentRecord wrapper
+ * Creates shared export processing primitives to be used both Location and Archive usecases of cjs
+ *
+ * @param {Object} moduleEnvironmentRecord
+ * @param {Compartment} compartment
+ * @param {Record<string, string>} resolvedImports
+ * @returns {{
+ * module: { exports: any },
+ * moduleExports: any,
+ * afterExecute: Function,
+ * require: Function,
+ * }}
+ */
+export const wrap = (moduleEnvironmentRecord, compartment, resolvedImports) => {
+ // This initial default value makes things like exports.hasOwnProperty() work in cjs.
+ moduleEnvironmentRecord.default = create(
+ compartment.globalThis.Object.prototype,
+ );
+
+ // Set all exported properties on the defult and on the namespace simultaneously for import *
+ // Root namespace is only accessible for imports. Requiring from cjs gets the default field of the namespace
+ const assignProp = (prop, value) => {
+ // TODO Consider __esModule handling
+ if (prop === '__esModule') {
+ return;
+ }
+ if (prop !== 'default') {
+ moduleEnvironmentRecord[prop] = value;
+ }
+ moduleEnvironmentRecord.default[prop] = value;
+ };
+
+ const originalExports = new Proxy(Object.create(null), {
+ get(target, prop) {
+ return moduleEnvironmentRecord.default[prop];
+ },
+ set(_target, prop, value) {
+ assignProp(prop, value);
+ return true;
+ },
+ defineProperty(_target, prop, descriptor) {
+ // Object.defineProperty(moduleEnvironmentRecord, prop, descriptor) would work if someone in the npm
+ // community didn't come up with the idea to redefine property in the getter.
+ if (has(descriptor, 'value')) {
+ // This will result in non-enumerable properties being enumerable, but it's better than undefined
+ assignProp(prop, descriptor.value);
+ } else {
+ // Running the getter defeats some lazy initialization tricks.
+ // Whoever depends on getters on exports for lazy init instead
+ // of factory functions should face the consequences.
+ assignProp(prop, descriptor.get ? descriptor.get() : undefined);
+ }
+ return true;
+ },
+ });
+
+ let finalExports = originalExports;
+
+ const module = freeze({
+ get exports() {
+ return finalExports;
+ },
+ set exports(value) {
+ finalExports = value;
+ },
+ });
+
+ const require = freeze((/** @type {string} */ importSpecifier) => {
+ const namespace = compartment.importNow(resolvedImports[importSpecifier]);
+ // If you read this file carefully, you'll see it's not possible for a cjs module to not have the default anymore.
+ // It's currently possible to require modules that were not created by this file though.
+ if (has(namespace, 'default')) {
+ return namespace.default;
+ } else {
+ return namespace;
+ }
+ });
+
+ const afterExecute = () => {
+ const exportsHaveBeenOverwritten = finalExports !== originalExports;
+ // Promotes keys from redefined module.export to top level namespace for import *
+ // Note: We could do it less consistently but closer to how node does it if we iterated over exports detected by
+ // the lexer.
+ if (exportsHaveBeenOverwritten) {
+ moduleEnvironmentRecord.default = finalExports;
+ keys(moduleEnvironmentRecord.default).forEach(prop => {
+ if (prop !== 'default')
+ moduleEnvironmentRecord[prop] = moduleEnvironmentRecord.default[prop];
+ });
+ }
+ };
+
+ return {
+ module,
+ moduleExports: originalExports,
+ afterExecute,
+ require,
+ };
+};

MODIFY
packages/compartment-mapper/src/parse-cjs.js
packages/compartment-mapper/src/parse-cjs.js
// @ts-check
import { analyzeCommonJS } from '@endo/cjs-module-analyzer';
+import { wrap } from './parse-cjs-shared-export-wrapper.js';
const textDecoder = new TextDecoder();
-const { apply } = Reflect;
-const { freeze, keys, create, hasOwnProperty } = Object;
-
-/**
- * @param {Object} object
- * @param {string} key
- * @returns {boolean}
- */
-const has = (object, key) => apply(hasOwnProperty, object, [key]);
-
-function namespaceDefaultIsACopyOfRoot(namespace) {
- const ns = new Set(keys(namespace));
- const ds = new Set(['default'].concat(keys(namespace.default)));
- if (ns.size !== ds.size) return false;
- for (const a of ns) if (!ds.has(a)) return false;
- return true;
-}
+const { freeze } = Object;
/** @type {import('./types.js').ParseFn} */
export const parseCjs = async (
@@ -50,95 +35,21 @@ export const parseCjs = async (
`(function (require, exports, module, __filename, __dirname) { ${source} //*/\n})\n//# sourceURL=${location}`,
);
- const originalExports = new Proxy(
- create(compartment.globalThis.Object.prototype),
- {
- get(target, prop) {
- // this makes things like exports.hasOwnProperty() work.
- return moduleEnvironmentRecord[prop] || target[prop];
- },
- set(_target, prop, value) {
- moduleEnvironmentRecord[prop] = value;
- return true;
- },
- defineProperty(_target, prop, descriptor) {
- // Object.defineProperty(moduleEnvironmentRecord, prop, descriptor) would work if someone in the npm
- // community didn't come up with the idea to redefine property in the getter.
- if (has(descriptor, 'value')) {
- // This will result in non-enumerable properties being enumerable, but it's better than undefined
- moduleEnvironmentRecord[prop] = descriptor.value;
- } else {
- // Running the getter defeats some lazy initialization tricks.
- // Whoever depends on getters on exports for lazy init instead
- // of factory functions should face the consequences.
- moduleEnvironmentRecord[prop] = descriptor.get
- ? descriptor.get()
- : undefined;
- }
- return true;
- },
- },
+ const { require, moduleExports, module, afterExecute } = wrap(
+ moduleEnvironmentRecord,
+ compartment,
+ resolvedImports,
);
- let finalExports = originalExports;
-
- const module = freeze({
- get exports() {
- return finalExports;
- },
- set exports(value) {
- finalExports = value;
- },
- });
-
- const require = freeze((/** @type {string} */ importSpecifier) => {
- const namespace = compartment.importNow(resolvedImports[importSpecifier]);
- if (
- // if cjs modules form a circular dependency and also mess with 'default', this may break
- namespace.default !== undefined &&
- // This is the closest we can get to checking if default was implicit. If someone sets default field to an
- // object with exactly the same fields as exported individually, it's probably safe to assume the values will
- // match as well.
- (keys(namespace).length === 1 ||
- namespaceDefaultIsACopyOfRoot(namespace))
- ) {
- return namespace.default;
- }
-
- // namespace.constructor === Object is false in Endo and true in Node.
- // If we want it fixed, need to wrap with a proxy or make a copy here.
- return namespace;
- });
-
functor(
require,
- finalExports,
+ moduleExports,
module,
location, // __filename
new URL('./', location).toString(), // __dirname
);
- const exportsHaveBeenOverwritten = finalExports !== originalExports;
- if (!exportsHaveBeenOverwritten) {
- // If the proxy is still there, all of the fields were collected, so we need a copy of moduleEnvironmentRecord
- // This will not work for non-enumerable properties, but how would moduleEnvironmentRecord get any?
- finalExports = Object.assign(
- create(compartment.globalThis.Object.prototype),
- moduleEnvironmentRecord,
- );
- }
-
- moduleEnvironmentRecord.default = finalExports;
-
- // Promotes keys from default to top level namespace for import *
- // Note: We could do it less consistently but closer to how node does it if we iterated over exports detected by
- // the lexer.
- if (exportsHaveBeenOverwritten) {
- keys(moduleEnvironmentRecord.default).forEach(k => {
- if (k !== 'default')
- moduleEnvironmentRecord[k] = moduleEnvironmentRecord.default[k];
- });
- }
+ afterExecute();
};
return {

MODIFY
packages/compartment-mapper/src/parse-pre-cjs.js
packages/compartment-mapper/src/parse-pre-cjs.js
// @ts-check
import { parseLocatedJson } from './json.js';
+import { wrap } from './parse-cjs-shared-export-wrapper.js';
const textDecoder = new TextDecoder();
-const { apply } = Reflect;
-const { freeze, keys, create, hasOwnProperty } = Object;
-
-/**
- * @param {Object} object
- * @param {string} key
- * @returns {boolean}
- */
-const has = (object, key) => apply(hasOwnProperty, object, [key]);
-
-function namespaceDefaultIsACopyOfRoot(namespace) {
- const ns = new Set(keys(namespace));
- const ds = new Set(['default'].concat(keys(namespace.default)));
- if (ns.size !== ds.size) return false;
- for (const a of ns) if (!ds.has(a)) return false;
- return true;
-}
-
const locationParent = location => {
const index = location.lastIndexOf('/');
if (index >= 0) {
@@ -51,95 +34,21 @@ export const parsePreCjs = async (
const execute = (moduleEnvironmentRecord, compartment, resolvedImports) => {
const functor = compartment.evaluate(source);
- const originalExports = new Proxy(
- create(compartment.globalThis.Object.prototype),
- {
- get(target, prop) {
- // this makes things like exports.hasOwnProperty() work.
- return moduleEnvironmentRecord[prop] || target[prop];
- },
- set(_target, prop, value) {
- moduleEnvironmentRecord[prop] = value;
- return true;
- },
- defineProperty(_target, prop, descriptor) {
- // Object.defineProperty(moduleEnvironmentRecord, prop, descriptor) would work if someone in the npm
- // community didn't come up with the idea to redefine property in the getter.
- if (has(descriptor, 'value')) {
- // This will result in non-enumerable properties being enumerable, but it's better than undefined
- moduleEnvironmentRecord[prop] = descriptor.value;
- } else {
- // Running the getter defeats some lazy initialization tricks.
- // Whoever depends on getters on exports for lazy init instead
- // of factory functions should face the consequences.
- moduleEnvironmentRecord[prop] = descriptor.get
- ? descriptor.get()
- : undefined;
- }
- return true;
- },
- },
+ const { require, moduleExports, module, afterExecute } = wrap(
+ moduleEnvironmentRecord,
+ compartment,
+ resolvedImports,
);
- let finalExports = originalExports;
-
- const module = freeze({
- get exports() {
- return finalExports;
- },
- set exports(value) {
- finalExports = value;
- },
- });
-
- const require = freeze((/** @type {string} */ importSpecifier) => {
- const namespace = compartment.importNow(resolvedImports[importSpecifier]);
- if (
- // if cjs modules form a circular dependency and also mess with 'default', this may break
- namespace.default !== undefined &&
- // This is the closest we can get to checking if default was implicit. If someone sets default field to an
- // object with exactly the same fields as exported individually, it's probably safe to assume the values will
- // match as well.
- (keys(namespace).length === 1 ||
- namespaceDefaultIsACopyOfRoot(namespace))
- ) {
- return namespace.default;
- }
-
- // namespace.constructor === Object is false in Endo and true in Node.
- // If we want it fixed, need to wrap with a proxy or make a copy here.
- return namespace;
- });
-
functor(
require,
- finalExports,
+ moduleExports,
module,
location, // __filename
locationParent(location), // __dirname
);
- const exportsHaveBeenOverwritten = finalExports !== originalExports;
- if (!exportsHaveBeenOverwritten) {
- // If the proxy is still there, all of the fields were collected, so we need a copy of moduleEnvironmentRecord
- // This will not work for non-enumerable properties, but how would moduleEnvironmentRecord get any?
- finalExports = Object.assign(
- create(compartment.globalThis.Object.prototype),
- moduleEnvironmentRecord,
- );
- }
-
- moduleEnvironmentRecord.default = finalExports;
-
- // Promotes keys from default to top level namespace for import *
- // Note: We could do it less consistently but closer to how node does it if we iterated over exports detected by
- // the lexer.
- if (exportsHaveBeenOverwritten) {
- keys(moduleEnvironmentRecord.default).forEach(k => {
- if (k !== 'default')
- moduleEnvironmentRecord[k] = moduleEnvironmentRecord.default[k];
- });
- }
+ afterExecute();
};
return {

ADD
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/after-exec/README.md
+This package changes exports of a required module a.js therefore changing the shape of the default export after a.js has finished its execution.
\ No newline at end of file

ADD
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/after-exec/a.js
+module.exports = { a: 1 };

ADD
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/after-exec/later.js
+module.exports.a = require('./a');

ADD
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/after-exec/main.js
+const a = require('./a');
+a.afterExec = 1;
+const later = require('./later');
+module.exports = { isOk: a === later.a };

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

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
@@ -5,6 +5,7 @@ const stableOrderJson = require('./assertion-generators/json.cjs');
const cycle1 = require('./cycle1');
const defineprop = require('defineprop');
const parserStruggles = require('parser-struggles');
+const afterExec = require('after-exec');
require('default-difficulties');
// Generated by running v16.13.2: node assertion-generators/expected.cjs
@@ -27,6 +28,11 @@ function assertInteropNameCollisions({ moduleReference, expect, name }) {
}
module.exports.assertions = {
+ defaultChangesAfterExec() {
+ if (!afterExec.isOk) {
+ throw Error('Expected a.js to be the same thing when required twice');
+ }
+ },
packageWithDefineProperty() {
if (!defineprop.isOk) {
throw Error('Expected exports created by defineProperty to be available');

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
"defineprop": "^1.0.0",
"interops": "^1.0.0",
"parser-struggles": "^1.0.0",
- "default-difficulties": "^1.0.0"
+ "default-difficulties": "^1.0.0",
+ "after-exec": "^1.0.0"
}
}

MODIFY
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/defineprop/example-elliptic.js
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/defineprop/example-elliptic.js
@@ -23,5 +23,5 @@ function defineCurve(name) {
});
}
-defineCurve('p192')
-defineCurve('secp256k1')
\ No newline at end of file
+defineCurve('p192');
+defineCurve('secp256k1');

MODIFY
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/defineprop/main.js
packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/defineprop/main.js
@@ -2,16 +2,18 @@ const curves = require('./example-elliptic');
const ethereum = require('./example-ethereum-util');
if (ethereum.isOk !== 1) {
- throw Error('Expected example-ethereum-util.js to export isOk')
+ throw Error('Expected example-ethereum-util.js to export isOk');
}
// elliptic expects this to work
if (
!Object.prototype.hasOwnProperty.call(curves, 'p192') ||
!Object.prototype.hasOwnProperty.call(curves, 'secp256k1')
) {
- throw Error('Expected example-elliptic.js to export p192 and secp256k1')
+ throw Error('Expected example-elliptic.js to export p192 and secp256k1');
}
if (curves.p192.name !== 'p192' || curves.secp256k1.name !== 'secp256k1') {
- throw Error(`Expected example-elliptic.js exports to contain values: p192,secp256k1 got: ${curves.p192.name},${curves.secp256k1.name}`)
+ throw Error(
+ `Expected example-elliptic.js exports to contain values: p192,secp256k1 got: ${curves.p192.name},${curves.secp256k1.name}`,
+ );
}

MODIFY
packages/compartment-mapper/test/test-cjs-compat.js
packages/compartment-mapper/test/test-cjs-compat.js
@@ -17,6 +17,7 @@ const assertFixture = (t, { namespace }) => {
assertions.moduleWithDefaultField();
assertions.parserStruggles();
assertions.moduleWithCycle();
+ assertions.defaultChangesAfterExec();
t.pass();
};

refactor(compartment-mapper): rebuild cjs exports handling with the assumption module.exports is the default export