feat(compartment-mapper): Reenable CommonJS
This reverts commit
MODIFY
packages/compartment-mapper/README.md
packages/compartment-mapper/README.md
# Compartment mapper
-> :warning: CommonJS support is temporarily disabled, pending a solution for
-> heuristic static analysis that does not entrain any Node.js built-ins.
-
The compartment mapper builds _compartment maps_ for Node.js style
applications, finding their dependencies and describing how to create
[Compartments][] for each package in the application.

MODIFY
packages/compartment-mapper/package.json
packages/compartment-mapper/package.json
"test": "ava"
},
"dependencies": {
+ "@babel/parser": "^7.8.4",
+ "@babel/traverse": "^7.8.4",
"ses": "^0.12.7"
},
"devDependencies": {

MODIFY
packages/compartment-mapper/src/node-modules.js
packages/compartment-mapper/src/node-modules.js
@@ -135,9 +135,9 @@ const findPackage = async (readDescriptor, directory, name) => {
}
};
-const languages = ['mjs', 'json'];
-const uncontroversialParsers = { mjs: 'mjs', json: 'json' };
-const commonParsers = uncontroversialParsers;
+const languages = ['mjs', 'cjs', 'json'];
+const uncontroversialParsers = { cjs: 'cjs', mjs: 'mjs', json: 'json' };
+const commonParsers = { js: 'cjs', ...uncontroversialParsers };
const moduleParsers = { js: 'mjs', ...uncontroversialParsers };
/**
@@ -152,7 +152,7 @@ const inferParsers = (descriptor, location) => {
throw new Error(
`Cannot interpret parser map ${JSON.stringify(
parsers,
- )} of package at ${location}, must be an object mapping file extensions to corresponding languages (mjs for ECMAScript modules or json for JSON modules`,
+ )} of package at ${location}, must be an object mapping file extensions to corresponding languages (mjs for ECMAScript modules, cjs for CommonJS modules, or json for JSON modules`,
);
}
const invalidLanguages = values(parsers).filter(
@@ -162,7 +162,7 @@ const inferParsers = (descriptor, location) => {
throw new Error(
`Cannot interpret parser map language values ${JSON.stringify(
invalidLanguages,
- )} of package at ${location}, must be an object mapping file extensions to corresponding languages (mjs for ECMAScript modules or json for JSON modules`,
+ )} of package at ${location}, must be an object mapping file extensions to corresponding languages (mjs for ECMAScript modules, cjs for CommonJS modules, or json for JSON modules`,
);
}
return { ...uncontroversialParsers, ...parsers };

ADD
packages/compartment-mapper/src/parse-requires.js
+import parser from '@babel/parser';
+import traverse from '@babel/traverse';
+
+/* parseRequires
+ * Analyzes a CommonJS module's obvious static shallow dependencies, returning
+ * an array of every module specifier that the module requires with a string
+ * literal.
+ *
+ * Does not differentiate conditional dependencies, which will likely cause
+ * errors in the loading phase. For example, `if (isNode) { require('fs') }`
+ * will reveal an unconditional dependency on a possibly unloadable `fs`
+ * module, even though the module has a contingency when that module is not
+ * available.
+ *
+ * Does not discover dynamic dependencies, which will likely cause errors in
+ * 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.
+ *
+ * @typedef ImportSpecifier string
+ * @param {string} source
+ * @param {string} location
+ * @return {Array<ImportSpecifier>}
+ */
+export const parseRequires = (source, location, packageLocation) => {
+ try {
+ const ast = parser.parse(source);
+ const required = new Set();
+ traverse.default(ast, {
+ CallExpression(path) {
+ const { node, scope } = path;
+ const { callee, arguments: args } = node;
+ if (callee.name !== 'require') {
+ return;
+ }
+ // We do not recognize `require()` or `require("id", true)`.
+ if (args.length !== 1) {
+ return;
+ }
+ // We only consider the form `require("id")`, not `require(expression)`.
+ const [specifier] = args;
+ if (specifier.type !== 'StringLiteral') {
+ return;
+ }
+ // The existence of a require variable in any parent scope indicates that
+ // this is not a free-variable use of the term `require`, so it does not
+ // likely refer to the module's given `require`.
+ if (scope.hasBinding('require')) {
+ return;
+ }
+ required.add(specifier.value);
+ },
+ });
+ return Array.from(required).sort();
+ } catch (error) {
+ if (/import/.exec(error.message) !== null) {
+ throw new Error(
+ `Cannot parse CommonJS module at ${location}, consider adding "type": "module" to package.json in ${packageLocation}: ${error}`,
+ );
+ }
+ throw new Error(`Cannot parse CommonJS module at ${location}: ${error}`);
+ }
+};

MODIFY
packages/compartment-mapper/src/parse.js
packages/compartment-mapper/src/parse.js
// @ts-check
/// <reference types="ses" />
+import { parseRequires } from './parse-requires.js';
import { parseExtension } from './extension.js';
import * as json from './json.js';
@@ -48,6 +49,7 @@ export const parseJson = async (
const source = textDecoder.decode(bytes);
/** @type {Readonly<Array<string>>} */
const imports = freeze([]);
+
/**
* @param {Object} exports
*/
@@ -61,9 +63,72 @@ export const parseJson = async (
};
};
+/** @type {ParseFn} */
+export const parseCjs = async (
+ bytes,
+ _specifier,
+ location,
+ packageLocation,
+) => {
+ const source = textDecoder.decode(bytes);
+
+ if (typeof location !== 'string') {
+ throw new TypeError(
+ `Cannot create CommonJS static module record, module location must be a string, got ${location}`,
+ );
+ }
+
+ const imports = parseRequires(source, location, packageLocation);
+ /**
+ * @param {Object} exports
+ * @param {Compartment} compartment
+ * @param {Record<string, string>} resolvedImports
+ */
+ const execute = async (exports, compartment, resolvedImports) => {
+ const functor = compartment.evaluate(
+ `(function (require, exports, module, __filename, __dirname) { ${source} //*/\n})\n//# sourceURL=${location}`,
+ );
+
+ let moduleExports = exports;
+
+ const module = freeze({
+ get exports() {
+ return moduleExports;
+ },
+ set exports(namespace) {
+ moduleExports = namespace;
+ exports.default = namespace;
+ },
+ });
+
+ const require = freeze(importSpecifier => {
+ const namespace = compartment.importNow(resolvedImports[importSpecifier]);
+ if (namespace.default !== undefined) {
+ return namespace.default;
+ }
+ return namespace;
+ });
+
+ functor(
+ require,
+ exports,
+ module,
+ location, // __filename
+ new URL('./', location).toString(), // __dirname
+ );
+ };
+
+ return {
+ parser: 'cjs',
+ bytes,
+ record: freeze({ imports, execute }),
+ };
+};
+
/** @type {Record<string, ParseFn>} */
export const parserForLanguage = {
mjs: parseMjs,
+ cjs: parseCjs,
json: parseJson,
};

MODIFY
packages/compartment-mapper/test/app.agar
packages/compartment-mapper/test/app.agar
Binary files a/packages/compartment-mapper/test/app.agar and b/packages/compartment-mapper/test/app.agar differ

MODIFY
packages/compartment-mapper/test/node_modules/app/main.js
packages/compartment-mapper/test/node_modules/app/main.js
/* global globalProperty, globalLexical */
+import typemodule from 'typemodule';
+import typecommon from 'typecommon';
+import typehybrid from 'typehybrid';
+import typeparsers from 'typeparsers';
+
+export { typemodule, typecommon, typehybrid, typeparsers };
+
export { avery } from 'avery';
// My (kriskowal) theory is that the brooke module isn't resolvable because the
// linter isn't aware of package.json's main property and seems to hope that
@@ -7,6 +14,7 @@ export { avery } from 'avery';
// eslint-disable-next-line import/no-unresolved
export { brooke } from 'brooke';
export { clarke } from 'clarke';
+export { danny } from 'danny';
// The builtin export does not resolve because it does not exist on disk and is
// a figment of testing machinations.

MODIFY
packages/compartment-mapper/test/node_modules/app/package.json
packages/compartment-mapper/test/node_modules/app/package.json
"dependencies": {
"avery": "^1.0.0",
"brooke": "^1.0.0",
- "clarke": "^1.0.0"
+ "clarke": "^1.0.0",
+ "danny": "^1.0.0",
+ "typecommon": "^1.0.0",
+ "typemodule": "^1.0.0",
+ "typehybrid": "^1.0.0",
+ "typeparsers": "^1.0.0"
}
}

MODIFY
packages/compartment-mapper/test/node_modules/danny/package.json
packages/compartment-mapper/test/node_modules/danny/package.json
"name": "danny",
"version": "1.0.0",
"main": "./src/index.js",
- "type": "commonjs",
+ "type": "module",
"parsers": {
"js": "mjs"
}

MODIFY
packages/compartment-mapper/test/test-main.js
packages/compartment-mapper/test/test-main.js
@@ -30,14 +30,20 @@ const assertFixture = (t, namespace) => {
avery,
brooke,
clarke,
+ danny,
builtin,
receivedGlobalProperty,
receivedGlobalLexical,
+ typecommon,
+ typemodule,
+ typehybrid,
+ typeparsers,
} = namespace;
t.is(avery, 'Avery', 'exports avery');
t.is(brooke, 'Brooke', 'exports brooke');
t.is(clarke, 'Clarke', 'exports clarke');
+ t.is(danny, 'Danny', 'exports danny');
t.is(builtin, 'builtin', 'exports builtin');
@@ -47,9 +53,25 @@ const assertFixture = (t, namespace) => {
globalLexicals.globalLexical,
'exports global lexical',
);
+ t.deepEqual(
+ typecommon,
+ [42, 42, 42, 42],
+ 'type=common package carries exports',
+ );
+ t.deepEqual(
+ typemodule,
+ [42, 42, 42, 42],
+ 'type=module package carries exports',
+ );
+ t.deepEqual(
+ typeparsers,
+ [42, 42, 42, 42],
+ 'parsers-specifying package carries exports',
+ );
+ t.is(typehybrid, 42, 'type=module and module= package carries exports');
};
-const fixtureAssertionCount = 6;
+const fixtureAssertionCount = 11;
// The "create builtin" test prepares a builtin module namespace object that
// gets threaded into all subsequent tests to satisfy the "builtin" module

ADD
packages/compartment-mapper/test/test-parse-requires.js
+import test from 'ava';
+import { parseRequires } from '../src/parse-requires.js';
+
+test('parse unique require calls', t => {
+ t.plan(1);
+ const code = `
+ require("b"); // sorted later
+
+ // require("bogus"); // not discovered
+
+ function square(require) {
+ require("shadowed"); // over-shadowed by argument
+ }
+
+ require("a");
+
+ function c() {
+ require("c"); // found despite inner scope
+ }
+
+ require("a"); // de-duplicated
+
+ require( "d" ); // such space
+
+ require("bogus", "bogus", "bogus");
+
+ require("id\\""); // found, despite inner quote
+ `;
+ const requires = parseRequires(code);
+ t.deepEqual(requires, ['a', 'b', 'c', 'd', 'id"']);
+});

feat(compartment-mapper): Reenable CommonJS

This reverts commit 8d7fb04f18acf49e22850576dded8bf7b7045548.