Skip to content

Commit aef5ab0

Browse files
committed
feat(compartment-mapper): expose Babel-based CJS parser w/ dynamic import support
Expose Babel-based CJS parser, `parse-cjs-babel`. Expose shared functionality for wrapping CJS functors with `__dirname`, `__filename`, etc. Add support for dynamic `import()` (`parse-cjs-babel` only).
1 parent b7addca commit aef5ab0

11 files changed

Lines changed: 280 additions & 50 deletions

File tree

.changeset/famous-seals-pick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@endo/compartment-mapper': minor
3+
---
4+
5+
Expose Babel-based CJS parser, `parse-cjs-babel`. Expose shared functionality for wrapping CJS functors with `__dirname`, `__filename`, etc. Add support for dynamic `import()` (`parse-cjs-babel` only).

packages/compartment-mapper/cjs.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// eslint-disable-next-line import/export
2+
export * from './src/types-external.js';
3+
4+
export * from './src/cjs.js';
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
// eslint-disable-next-line import/export -- just types
22
export * from './src/types-external.js';
33

4-
export { defaultParserForLanguage } from './src/import-parsers.js';
4+
export {
5+
defaultParserForLanguage,
6+
parserForLanguageWithCjsBabel,
7+
} from './src/import-parsers.js';

packages/compartment-mapper/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"main": "./index.js",
2424
"exports": {
2525
".": "./index.js",
26+
"./cjs.js": "./cjs.js",
2627
"./import.js": "./import.js",
2728
"./import-lite.js": "./import-lite.js",
2829
"./import-parsers.js": "./import-parsers.js",
@@ -45,6 +46,7 @@
4546
"./script-lite.js": "./script-lite.js",
4647
"./node-powers.js": "./node-powers.js",
4748
"./node-modules.js": "./node-modules.js",
49+
"./policy.js": "./policy.js",
4850
"./package.json": "./package.json"
4951
},
5052
"scripts": {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/* eslint-disable no-underscore-dangle */
2+
/**
3+
* Provides {@link buildCjsExecuteRecord}, a function that converts a
4+
* {@link CjsModuleSourceRecord} into a {@link FinalStaticModuleType}.
5+
*
6+
* For use with `@endo/parser-pipeline`'s `createComposedParser()`.
7+
*
8+
* @module
9+
*/
10+
11+
import { getModulePaths, wrap } from './parse-cjs-shared-export-wrapper.js';
12+
13+
/**
14+
* @import {CjsModuleSourceRecord} from '@endo/module-source'
15+
* @import {ReadFn, ReadPowers} from './types.js'
16+
* @import {FinalStaticModuleType} from 'ses'
17+
*/
18+
19+
const { freeze } = Object;
20+
21+
/**
22+
* Converts a {@link CjsModuleSourceRecord} (which has a `cjsFunctor` string)
23+
* into a `FinalStaticModuleType`-compatible record (which has an `execute`
24+
* function). This is the bridge between the composed-pipeline CJS analysis and
25+
* the compartment-mapper execution model.
26+
*
27+
* Used by both {@link parseCjsBabel} (single-shot parser) and
28+
* {@link createCjsExecParser} (composed-pipeline parser) so the execution
29+
* logic lives in exactly one place.
30+
*
31+
* @param {CjsModuleSourceRecord} cjsRecord
32+
* @param {string} location
33+
* @param {ReadFn | ReadPowers | undefined} readPowers
34+
* @returns {FinalStaticModuleType}
35+
*/
36+
37+
export const buildCjsExecuteRecord = (cjsRecord, location, readPowers) => {
38+
const { filename, dirname } = getModulePaths(readPowers, location);
39+
40+
/**
41+
* @param {object} moduleEnvironmentRecord
42+
* @param {Compartment} compartment
43+
* @param {Record<string, string>} resolvedImports
44+
*/
45+
const execute = (moduleEnvironmentRecord, compartment, resolvedImports) => {
46+
const functor = compartment.evaluate(cjsRecord.cjsFunctor);
47+
48+
const wrapResult = wrap({
49+
moduleEnvironmentRecord,
50+
compartment,
51+
resolvedImports,
52+
location,
53+
readPowers,
54+
});
55+
56+
const args = [
57+
wrapResult.require,
58+
wrapResult.moduleExports,
59+
wrapResult.module,
60+
filename,
61+
dirname,
62+
];
63+
64+
if (cjsRecord.__needsImport__ && wrapResult.importFn) {
65+
args.push(wrapResult.importFn);
66+
}
67+
68+
functor.call(wrapResult.moduleExports, ...args);
69+
70+
wrapResult.afterExecute();
71+
};
72+
73+
return freeze({
74+
imports: cjsRecord.imports,
75+
exports: cjsRecord.exports,
76+
reexports: cjsRecord.reexports,
77+
execute,
78+
});
79+
};

packages/compartment-mapper/src/import-parsers.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ import parserText from './parse-text.js';
1111
import parserBytes from './parse-bytes.js';
1212
import parserCjs from './parse-cjs.js';
1313
import parserMjs from './parse-mjs.js';
14+
import parserCjsBabel from './parse-cjs-babel.js';
15+
16+
const { freeze } = Object;
1417

1518
/** @satisfies {Readonly<ParserForLanguage>} */
16-
export const defaultParserForLanguage = Object.freeze(
19+
export const defaultParserForLanguage = freeze(
1720
/** @type {const} */ ({
1821
mjs: parserMjs,
1922
cjs: parserCjs,
@@ -22,3 +25,11 @@ export const defaultParserForLanguage = Object.freeze(
2225
bytes: parserBytes,
2326
}),
2427
);
28+
29+
/** @satisfies {Readonly<ParserForLanguage>} */
30+
export const parserForLanguageWithCjsBabel = freeze(
31+
/** @type {const} */ ({
32+
...defaultParserForLanguage,
33+
cjs: parserCjsBabel,
34+
}),
35+
);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/* eslint-disable no-underscore-dangle */
2+
/**
3+
* Provides language behavior (parser) for importing CommonJS as a virtual
4+
* module source, using Babel AST analysis instead of the character-level lexer.
5+
*
6+
* Drop-in replacement for {@link parse-cjs.js}. Consumers opt in via the
7+
* pre-built parser map:
8+
*
9+
* ```js
10+
* import { parserForLanguageWithCjsBabel } from '@endo/compartment-mapper/import-parsers.js';
11+
*
12+
* await importLocation(readPowers, entryUrl, {
13+
* parserForLanguage: parserForLanguageWithCjsBabel,
14+
* });
15+
* ```
16+
*
17+
* @module
18+
*/
19+
20+
/**
21+
* @import {ParseFn, ParserImplementation} from './types.js'
22+
*/
23+
24+
import { CjsModuleSource } from '@endo/module-source';
25+
import { buildCjsExecuteRecord } from './cjs.js';
26+
27+
const textDecoder = new TextDecoder();
28+
29+
/** @type {ParseFn} */
30+
export const parseCjsBabel = (
31+
bytes,
32+
_specifier,
33+
location,
34+
_packageLocation,
35+
{ readPowers } = {},
36+
) => {
37+
const source = textDecoder.decode(bytes);
38+
const cjsRecord = new CjsModuleSource(source, { sourceUrl: location });
39+
40+
return {
41+
parser: 'cjs',
42+
bytes,
43+
record: buildCjsExecuteRecord(cjsRecord, location, readPowers),
44+
};
45+
};
46+
47+
/** @type {ParserImplementation} */
48+
export default {
49+
parse: parseCjsBabel,
50+
heuristicImports: true,
51+
synchronous: true,
52+
};

packages/compartment-mapper/src/parse-cjs-shared-export-wrapper.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,11 @@ export const getModulePaths = (readPowers, location) => {
8383
* @param {string} in.location
8484
* @param {ReadFn | ReadPowers | undefined} in.readPowers
8585
* @returns {{
86-
* module: { exports: any },
87-
* moduleExports: any,
88-
* afterExecute: Function,
89-
* require: Function,
86+
* module: { exports: unknown },
87+
* moduleExports: unknown,
88+
* afterExecute: () => void,
89+
* require: (specifier: string) => unknown,
90+
* importFn: (specifier: string) => Promise<unknown>,
9091
* }}
9192
*/
9293
export const wrap = ({
@@ -204,6 +205,15 @@ export const wrap = ({
204205

205206
freeze(require);
206207

208+
/** @param {string} importSpecifier */
209+
const importFn = async importSpecifier => {
210+
const specifier = has(resolvedImports, importSpecifier)
211+
? resolvedImports[importSpecifier]
212+
: importSpecifier;
213+
return compartment.import(specifier);
214+
};
215+
freeze(importFn);
216+
207217
const afterExecute = () => {
208218
const finalExports = module.exports; // in case it's a getter, only call it once
209219
const exportsHaveBeenOverwritten = finalExports !== originalExports;
@@ -231,5 +241,6 @@ export const wrap = ({
231241
moduleExports: originalExports,
232242
afterExecute,
233243
require,
244+
importFn,
234245
};
235246
};

0 commit comments

Comments
 (0)