Skip to content

Commit ee18142

Browse files
committed
feat(compartment-mapper): add Babel-based CJS parser with import() support
- Remove `extends SourceMapHookDetails` from `TransformSourceParams`; `source` is only available at the `sourceMapHook` call site, not in the options/state bag. Inline the optional `sourceUrl`/`sourceMapUrl` fields directly. - Add `allowHidden?: boolean` to `TransformSourceParams` to match runtime usage in `babel-plugin.js`, removing the `@ts-expect-error`. - Make `SourceMapObject.file` optional per Source Map v3 spec. - Use full tuple `[name, false, undefined]` for `hoistedDecls.push()` instead of short `[name]`, removing the `@ts-expect-error`. - Remove stale `@ts-expect-error` directives for `@babel/generator` options now recognized by `@types/babel__generator`.
1 parent debd79c commit ee18142

11 files changed

Lines changed: 270 additions & 50 deletions

File tree

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/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: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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} from './types.js'
22+
* @import {ParserImplementation} from './types.js'
23+
*/
24+
25+
import { CjsModuleSource } from '@endo/module-source';
26+
import { wrap, getModulePaths } from './parse-cjs-shared-export-wrapper.js';
27+
28+
const textDecoder = new TextDecoder();
29+
30+
const { freeze } = Object;
31+
32+
/** @type {ParseFn} */
33+
export const parseCjsBabel = (
34+
bytes,
35+
_specifier,
36+
location,
37+
_packageLocation,
38+
{ readPowers } = {},
39+
) => {
40+
const source = textDecoder.decode(bytes);
41+
const cjsRecord = new CjsModuleSource(source, { sourceUrl: location });
42+
const { filename, dirname } = getModulePaths(readPowers, location);
43+
44+
/**
45+
* @param {object} moduleEnvironmentRecord
46+
* @param {Compartment} compartment
47+
* @param {Record<string, string>} resolvedImports
48+
*/
49+
const execute = (moduleEnvironmentRecord, compartment, resolvedImports) => {
50+
const functor = compartment.evaluate(cjsRecord.cjsFunctor);
51+
52+
const wrapResult = wrap({
53+
moduleEnvironmentRecord,
54+
compartment,
55+
resolvedImports,
56+
location,
57+
readPowers,
58+
});
59+
60+
const args = [
61+
wrapResult.require,
62+
wrapResult.moduleExports,
63+
wrapResult.module,
64+
filename,
65+
dirname,
66+
];
67+
68+
if (cjsRecord.__needsImport__ && wrapResult.importFn) {
69+
args.push(wrapResult.importFn);
70+
}
71+
72+
functor.call(wrapResult.moduleExports, ...args);
73+
74+
wrapResult.afterExecute();
75+
};
76+
77+
return {
78+
parser: 'cjs',
79+
bytes,
80+
record: freeze({
81+
imports: cjsRecord.imports,
82+
exports: cjsRecord.exports,
83+
reexports: cjsRecord.reexports,
84+
execute,
85+
}),
86+
};
87+
};
88+
89+
/** @type {ParserImplementation} */
90+
export default {
91+
parse: parseCjsBabel,
92+
heuristicImports: true,
93+
synchronous: true,
94+
};

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
};

packages/compartment-mapper/test/cjs-compat.test.js

Lines changed: 97 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ import 'ses';
66
import test from 'ava';
77
import path from 'path';
88
import { scaffold } from './scaffold.js';
9+
import {
10+
defaultParserForLanguage,
11+
parserForLanguageWithCjsBabel,
12+
} from '../src/import-parsers.js';
913

1014
/**
1115
* @import {FixtureAssertionFn} from './test.types.js';
16+
* @import {ThirdPartyStaticModuleInterface} from 'ses'
1217
*/
1318

1419
const fixture = new URL(
@@ -19,9 +24,13 @@ const fixtureDirname = new URL(
1924
'fixtures-cjs-compat/node_modules/app/dirname.js',
2025
import.meta.url,
2126
).toString();
27+
const fixtureDynamicImport = new URL(
28+
'fixtures-cjs-compat/node_modules/dynamic-import/index.js',
29+
import.meta.url,
30+
).toString();
2231

2332
const q = JSON.stringify;
24-
33+
const { freeze } = Object;
2534
/**
2635
* @type {FixtureAssertionFn<{requireResolvePaths: string[]}>}
2736
*/
@@ -70,50 +79,94 @@ const assertFixture = (t, { namespace, testCategoryHint }) => {
7079

7180
const fixtureAssertionCount = 2;
7281

73-
scaffold(
74-
'fixtures-cjs-compat',
75-
test,
76-
fixture,
77-
assertFixture,
78-
fixtureAssertionCount,
79-
);
82+
const parsersForLanguage = {
83+
default: defaultParserForLanguage,
84+
babel: parserForLanguageWithCjsBabel,
85+
};
86+
87+
for (const [name, parserForLanguage] of Object.entries(parsersForLanguage)) {
88+
scaffold(
89+
`fixtures-cjs-compat-${name}`,
90+
test,
91+
fixture,
92+
assertFixture,
93+
fixtureAssertionCount,
94+
{
95+
parserForLanguage,
96+
},
97+
);
8098

81-
// Exit module errors are also deferred
82-
scaffold(
83-
'fixtures-cjs-compat-exit-module',
84-
test,
85-
fixture,
86-
assertFixture,
87-
fixtureAssertionCount,
88-
{
89-
additionalOptions: {
90-
importHook: async specifier => {
91-
throw Error(`${q(specifier)} is NOT an exit module.`);
99+
// Exit module errors are also deferred
100+
scaffold(
101+
`fixtures-cjs-compat-exit-module-${name}`,
102+
test,
103+
fixture,
104+
assertFixture,
105+
fixtureAssertionCount,
106+
{
107+
additionalOptions: {
108+
importHook: async specifier => {
109+
throw Error(`${q(specifier)} is NOT an exit module.`);
110+
},
92111
},
112+
parserForLanguage,
93113
},
94-
},
95-
);
114+
);
96115

97-
scaffold(
98-
'fixtures-cjs-compat-__dirname',
99-
test,
100-
fixtureDirname,
101-
(t, { namespace, testCategoryHint }) => {
102-
if (testCategoryHint === 'Location') {
103-
const { __filename, __dirname } = namespace;
104-
t.is(__filename, path.join(__dirname, '/dirname.js'));
105-
t.assert(!__dirname.startsWith('file://'));
106-
t.notRegex(
107-
__dirname,
108-
/[\\/]$/,
109-
'Expected __dirname to NOT have a trailing slash',
110-
);
111-
} else {
112-
const { __filename, __dirname } = namespace;
113-
t.is(__dirname, null);
114-
t.is(__filename, null);
115-
t.pass();
116-
}
117-
},
118-
3,
119-
);
116+
scaffold(
117+
`fixtures-cjs-compat-__dirname-${name}`,
118+
test,
119+
fixtureDirname,
120+
(t, { namespace, testCategoryHint }) => {
121+
if (testCategoryHint === 'Location') {
122+
const { __filename, __dirname } = namespace;
123+
t.is(__filename, path.join(__dirname, '/dirname.js'));
124+
t.assert(!__dirname.startsWith('file://'));
125+
t.notRegex(
126+
__dirname,
127+
/[\\/]$/,
128+
'Expected __dirname to NOT have a trailing slash',
129+
);
130+
} else {
131+
const { __filename, __dirname } = namespace;
132+
t.is(__dirname, null);
133+
t.is(__filename, null);
134+
t.pass();
135+
}
136+
},
137+
3,
138+
{
139+
parserForLanguage,
140+
},
141+
);
142+
143+
scaffold(
144+
`fixtures-cjs-compat-dynamic-import-${name}`,
145+
test,
146+
fixtureDynamicImport,
147+
async (t, { namespace }) => {
148+
const { namespace: dynamicNamespace } =
149+
// @ts-expect-error - untyped
150+
await namespace.dynamicImport('a');
151+
t.is(dynamicNamespace.foo, 'foo');
152+
},
153+
1,
154+
{
155+
// NOTE: this should fail with parse-cjs, but not parse-cjs-babel
156+
knownFailure: name === 'default',
157+
parserForLanguage,
158+
additionalOptions: {
159+
importHook: async () => {
160+
/** @type {ThirdPartyStaticModuleInterface} */
161+
return freeze({
162+
imports: [],
163+
exports: ['foo'],
164+
execute: moduleExports => {
165+
moduleExports.foo = 'foo';
166+
},
167+
});
168+
},
169+
},
170+
},
171+
);
172+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import 'ses';
2+
3+
import test from 'ava';
4+
import { scaffold } from './scaffold.js';
5+
6+
const fixture = new URL(
7+
'fixtures-dynamic-import-esm/node_modules/app/index.js',
8+
import.meta.url,
9+
).toString();
10+
11+
scaffold(
12+
'fixtures-dynamic-import-esm',
13+
test,
14+
fixture,
15+
async (t, { namespace }) => {
16+
// @ts-expect-error - untyped
17+
const foo = await namespace.getFoo();
18+
t.is(foo, 'foo');
19+
},
20+
1,
21+
{
22+
knownArchiveFailure: true,
23+
},
24+
);

packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/dynamic-import/index.js

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compartment-mapper/test/fixtures-cjs-compat/node_modules/dynamic-import/package.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compartment-mapper/test/fixtures-dynamic-import-esm/node_modules/app/foo.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compartment-mapper/test/fixtures-dynamic-import-esm/node_modules/app/index.js

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)