feat(compartment-mapper): Pivot to CommonJS lexical analyzer
MODIFY
packages/compartment-mapper/package.json
packages/compartment-mapper/package.json
"test": "ava"
},
"dependencies": {
- "@babel/parser": "^7.8.4",
- "@babel/traverse": "^7.8.4",
+ "@endo/lexer": "^0.1.0",
"ses": "^0.12.7"
},
"devDependencies": {

DELETE
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 { analyzeCommonJS } from '@endo/lexer';
import { parseExtension } from './extension.js';
import * as json from './json.js';
@@ -59,7 +59,7 @@ export const parseJson = async (
return {
parser: 'json',
bytes,
- record: freeze({ imports, execute }),
+ record: freeze({ imports, exports: freeze(['default']), execute }),
};
};
@@ -68,7 +68,7 @@ export const parseCjs = async (
bytes,
_specifier,
location,
- packageLocation,
+ _packageLocation,
) => {
const source = textDecoder.decode(bytes);
@@ -78,26 +78,27 @@ export const parseCjs = async (
);
}
- const imports = parseRequires(source, location, packageLocation);
+ const { requires: imports, exports, reexports } = analyzeCommonJS(
+ source,
+ location,
+ );
+
/**
- * @param {Object} exports
+ * @param {Object} moduleExports
* @param {Compartment} compartment
* @param {Record<string, string>} resolvedImports
*/
- const execute = async (exports, compartment, resolvedImports) => {
+ const execute = async (moduleExports, 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;
+ set exports(value) {
+ moduleExports.default = value;
},
});
@@ -111,7 +112,7 @@ export const parseCjs = async (
functor(
require,
- exports,
+ moduleExports,
module,
location, // __filename
new URL('./', location).toString(), // __dirname
@@ -121,7 +122,7 @@ export const parseCjs = async (
return {
parser: 'cjs',
bytes,
- record: freeze({ imports, execute }),
+ record: freeze({ imports, exports, reexports, execute }),
};
};

DELETE
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): Pivot to CommonJS lexical analyzer