feat(compartment-mapper): add bundle support for commonjs
Co-authered-by: kumavis
ADD
packages/compartment-mapper/src/bundle-cjs.js
+/** quotes strings */
+const q = JSON.stringify;
+
+const exportsCellRecord = exportsList =>
+ ''.concat(
+ ...exportsList.map(
+ exportName => `\
+ ${exportName}: cell(${q(exportName)}),
+`,
+ ),
+ );
+
+// This function is serialized and references variables from its destination scope.
+const runtime = function wrapCjsFunctor(num) {
+ /* eslint-disable no-undef */
+ return ({ imports = {} }) => {
+ const cModule = Object.freeze(
+ Object.defineProperty({}, 'exports', cells[num].default),
+ );
+ // TODO: specifier not found handling
+ const requireImpl = specifier => cells[imports[specifier]].default.get();
+ functors[num](Object.freeze(requireImpl), cModule.exports, cModule);
+ Object.keys(cells[num])
+ .filter(k => k !== 'default' && k !== '*')
+ .map(k => cells[num][k].set(cModule.exports[k]));
+ };
+ /* eslint-enable no-undef */
+}.toString();
+
+export default {
+ runtime,
+ getBundlerKit({
+ index,
+ indexedImports,
+ record: { cjsFunctor, exports: exportsList = {} },
+ }) {
+ const importsMap = JSON.stringify(indexedImports);
+
+ return {
+ getFunctor: () => `\
+// === functors[${index}] ===
+${cjsFunctor},
+`,
+ getCells: () => `\
+ {
+${exportsCellRecord(exportsList)}\
+ },
+`,
+ getReexportsWiring: () => '',
+ getFunctorCall: () => `\
+ wrapCjsFunctor(${index})({imports: ${importsMap}});
+`,
+ };
+ },
+};

ADD
packages/compartment-mapper/src/bundle-mjs.js
+/** quotes strings */
+const q = JSON.stringify;
+
+const exportsCellRecord = exportMap =>
+ ''.concat(
+ ...Object.keys(exportMap).map(
+ exportName => `\
+ ${exportName}: cell(${q(exportName)}),
+`,
+ ),
+ );
+
+const importsCellSetter = (exportMap, index) =>
+ ''.concat(
+ ...Object.entries(exportMap).map(
+ ([exportName, [importName]]) => `\
+ ${importName}: cells[${index}].${exportName}.set,
+`,
+ ),
+ );
+
+const adaptReexport = reexportMap => {
+ if (!reexportMap) {
+ return {};
+ }
+ const ret = Object.fromEntries(
+ Object.values(reexportMap)
+ .flat()
+ .map(([local, exported]) => [exported, [local]]),
+ );
+ return ret;
+};
+
+export const runtime = `\
+function observeImports(map, importName, importIndex) {
+ for (const [name, observers] of map.get(importName)) {
+ const cell = cells[importIndex][name];
+ if (cell === undefined) {
+ throw new ReferenceError(\`Cannot import name \${name}\`);
+ }
+ for (const observer of observers) {
+ cell.observe(observer);
+ }
+ }
+}
+`;
+
+export default {
+ runtime,
+ getBundlerKit({
+ index,
+ indexedImports,
+ record: {
+ __syncModuleProgram__,
+ __fixedExportMap__ = {},
+ __liveExportMap__ = {},
+ __reexportMap__ = {},
+ reexports,
+ },
+ }) {
+ return {
+ getFunctor: () => `\
+// === functors[${index}] ===
+${__syncModuleProgram__},
+`,
+ getCells: () => `\
+ {
+${exportsCellRecord(__fixedExportMap__)}${exportsCellRecord(
+ __liveExportMap__,
+ )}${exportsCellRecord(adaptReexport(__reexportMap__))}\
+ },
+`,
+ getReexportsWiring: () => {
+ const mappings = reexports.map(
+ importSpecifier => `\
+ Object.defineProperties(cells[${index}], Object.getOwnPropertyDescriptors(cells[${indexedImports[importSpecifier]}]));
+`,
+ );
+ // Create references for export name as newname
+ const namedReexportsToProcess = Object.entries(__reexportMap__);
+ if (namedReexportsToProcess.length > 0) {
+ mappings.push(`
+ Object.defineProperties(cells[${index}], {${namedReexportsToProcess.map(
+ ([specifier, renames]) => {
+ return renames.map(
+ ([localName, exportedName]) =>
+ `${q(exportedName)}: { value: cells[${
+ indexedImports[specifier]
+ }][${q(localName)}] }`,
+ );
+ },
+ )} });
+`);
+ }
+ return mappings.join('');
+ },
+ getFunctorCall: () => `\
+ functors[${index}]({
+ imports(entries) {
+ const map = new Map(entries);
+ ${''.concat(
+ ...Object.entries(indexedImports).map(
+ ([importName, importIndex]) => `\
+ observeImports(map, ${q(importName)}, ${importIndex});
+ `,
+ ),
+ )}\
+ },
+ liveVar: {
+ ${importsCellSetter(__liveExportMap__, index)}\
+ },
+ onceVar: {
+${importsCellSetter(__fixedExportMap__, index)}\
+ },
+ importMeta: {},
+ });
+`,
+ };
+ },
+};

MODIFY
packages/compartment-mapper/src/bundle.js
packages/compartment-mapper/src/bundle.js
@@ -24,10 +24,10 @@ import parserArchiveCjs from './parse-archive-cjs.js';
import parserArchiveMjs from './parse-archive-mjs.js';
import { parseLocatedJson } from './json.js';
-const textEncoder = new TextEncoder();
+import mjsSupport from './bundle-mjs.js';
+import cjsSupport from './bundle-cjs.js';
-/** quotes strings */
-const q = JSON.stringify;
+const textEncoder = new TextEncoder();
/** @type {Record<string, ParserImplementation>} */
const parserForLanguage = {
@@ -40,18 +40,6 @@ const parserForLanguage = {
bytes: parserBytes,
};
-function adaptReexport(reexportMap) {
- if (!reexportMap) {
- return {};
- }
- const ret = Object.fromEntries(
- Object.values(reexportMap)
- .flat()
- .map(([local, exported]) => [exported, [local]]),
- );
- return ret;
-}
-
/**
* @param {Record<string, CompartmentDescriptor>} compartmentDescriptors
* @param {Record<string, CompartmentSources>} compartmentSources
@@ -139,6 +127,32 @@ const sortedModules = (
return modules;
};
+const implementationPerParser = {
+ 'pre-mjs-json': mjsSupport,
+ 'pre-cjs-json': cjsSupport,
+};
+
+function getRuntime(parser) {
+ return implementationPerParser[parser]
+ ? implementationPerParser[parser].runtime
+ : `/*unknown parser:${parser}*/`;
+}
+
+function getBundlerKitForModule(module) {
+ const parser = module.parser;
+ if (!implementationPerParser[parser]) {
+ const warning = `/*unknown parser:${parser}*/`;
+ // each item is a function to avoid creating more in-memory copies of the source text etc.
+ return {
+ getFunctor: () => `(()=>{${warning}})`,
+ getCells: `{${warning}}`,
+ getFunctorCall: warning,
+ };
+ }
+ const getBundlerKit = implementationPerParser[parser].getBundlerKit;
+ return getBundlerKit(module);
+}
+
/**
* @param {ReadFn} read
* @param {string} moduleLocation
@@ -222,6 +236,7 @@ export const makeBundle = async (read, moduleLocation, options) => {
module.index = index;
modulesByKey[module.key] = module;
}
+ const parsersInUse = new Set();
for (const module of modules) {
module.indexedImports = Object.fromEntries(
Object.entries(module.resolvedImports).map(([importSpecifier, key]) => [
@@ -229,53 +244,15 @@ export const makeBundle = async (read, moduleLocation, options) => {
modulesByKey[key].index,
]),
);
+ parsersInUse.add(module.parser);
+ module.bundlerKit = getBundlerKitForModule(module);
}
- // Only support mjs format.
- const problems = modules
- .filter(module => module.parser !== 'pre-mjs-json')
- .map(
- ({ moduleSpecifier, compartmentName, parser }) =>
- `module ${moduleSpecifier} in compartment ${compartmentName} in language ${parser}`,
- );
- if (problems.length) {
- throw new Error(
- `Can only bundle applications that only have ESM (.mjs-type) modules, got ${problems.join(
- ', ',
- )}`,
- );
- }
-
- const exportsCellRecord = exportMap =>
- ''.concat(
- ...Object.keys(exportMap).map(
- exportName => `\
- ${exportName}: cell(${q(exportName)}),
-`,
- ),
- );
- const importsCellSetter = (exportMap, index) =>
- ''.concat(
- ...Object.entries(exportMap).map(
- ([exportName, [importName]]) => `\
- ${importName}: cells[${index}].${exportName}.set,
-`,
- ),
- );
-
const bundle = `\
'use strict';
(() => {
const functors = [
-${''.concat(
- ...modules.map(
- ({ record: { __syncModuleProgram__ } }, i) =>
- `\
-// === functors[${i}] ===
-${__syncModuleProgram__},
-`,
- ),
-)}\
+${''.concat(...modules.map(m => m.bundlerKit.getFunctor()))}\
]; // functors end
const cell = (name, value = undefined) => {
@@ -299,49 +276,10 @@ ${__syncModuleProgram__},
};
const cells = [
-${''.concat(
- ...modules.map(
- ({
- record: { __fixedExportMap__, __liveExportMap__, __reexportMap__ },
- }) => `\
- {
-${exportsCellRecord(__fixedExportMap__)}${exportsCellRecord(
- __liveExportMap__,
- )}${exportsCellRecord(adaptReexport(__reexportMap__))}\
- },
-`,
- ),
-)}\
+${''.concat(...modules.map(m => m.bundlerKit.getCells()))}\
];
-${''.concat(
- ...modules.flatMap(
- ({ index, indexedImports, record: { reexports, __reexportMap__ } }) => {
- const mappings = reexports.map(
- importSpecifier => `\
- Object.defineProperties(cells[${index}], Object.getOwnPropertyDescriptors(cells[${indexedImports[importSpecifier]}]));
-`,
- );
- // Create references for export name as newname
- const namedReexportsToProcess = Object.entries(__reexportMap__);
- if (namedReexportsToProcess.length > 0) {
- mappings.push(`
- Object.defineProperties(cells[${index}], {${namedReexportsToProcess.map(
- ([specifier, renames]) => {
- return renames.map(
- ([localName, exportedName]) =>
- `${q(exportedName)}: { value: cells[${
- indexedImports[specifier]
- }][${q(localName)}] }`,
- );
- },
- )} });
- `);
- }
- return mappings;
- },
- ),
-)}\
+${''.concat(...modules.map(m => m.bundlerKit.getReexportsWiring()))}\
const namespaces = cells.map(cells => Object.freeze(Object.create(null, cells)));
@@ -349,47 +287,9 @@ ${''.concat(
cells[index]['*'] = cell('*', namespaces[index]);
}
- const observeImports = (map, importName, importIndex) => {
- for (const [name, observers] of map.get(importName)) {
- const cell = cells[importIndex][name];
- if (cell === undefined) {
- throw new ReferenceError(\`Cannot import name \${name}\`);
- }
- for (const observer of observers) {
- cell.observe(observer);
- }
- }
- };
+${''.concat(...Array.from(parsersInUse).map(parser => getRuntime(parser)))}
-${''.concat(
- ...modules.map(
- ({
- index,
- indexedImports,
- record: { __liveExportMap__, __fixedExportMap__ },
- }) => `\
- functors[${index}]({
- imports(entries) {
- const map = new Map(entries);
-${''.concat(
- ...Object.entries(indexedImports).map(
- ([importName, importIndex]) => `\
- observeImports(map, ${q(importName)}, ${importIndex});
-`,
- ),
-)}\
- },
- liveVar: {
-${importsCellSetter(__liveExportMap__, index)}\
- },
- onceVar: {
-${importsCellSetter(__fixedExportMap__, index)}\
- },
- importMeta: {},
- });
-`,
- ),
-)}\
+${''.concat(...modules.map(m => m.bundlerKit.getFunctorCall()))}\
return cells[cells.length - 1]['*'].get();
})();

MODIFY
packages/compartment-mapper/src/parse-archive-cjs.js
packages/compartment-mapper/src/parse-archive-cjs.js
@@ -30,15 +30,19 @@ export const parseArchiveCjs = async (
exports.push('default');
}
+ const cjsWrappedSource = `(function (require, exports, module, __filename, __dirname) { ${source} //*/\n})\n`;
+
const pre = textEncoder.encode(
JSON.stringify({
imports,
exports,
reexports,
- source: `(function (require, exports, module, __filename, __dirname) { ${source} //*/\n})\n`,
+ source: cjsWrappedSource,
}),
);
+ const cjsFunctor = `${cjsWrappedSource}//# sourceURL=${location}\n`;
+
return {
parser: 'pre-cjs-json',
bytes: pre,
@@ -48,6 +52,7 @@ export const parseArchiveCjs = async (
exports: freeze(exports),
reexports: freeze(reexports),
execute: noopExecute,
+ cjsFunctor,
})
),
};

ADD
packages/compartment-mapper/test/fixtures-0/node_modules/bundle/icando.cjs
+const zzz = require('./requireme.cjs').hello || 404;
+const fromMjs = require('./import-default-export-from-me');
+module.exports = { zzz, fromMjs };
\ No newline at end of file

MODIFY
packages/compartment-mapper/test/fixtures-0/node_modules/bundle/main.js
packages/compartment-mapper/test/fixtures-0/node_modules/bundle/main.js
@@ -24,3 +24,6 @@ import * as colors from './reexport-all.js';
print(colors);
import 'bundle-dep';
+
+import * as cjsstuff from './icando.cjs';
+print(cjsstuff)
\ No newline at end of file

ADD
packages/compartment-mapper/test/fixtures-0/node_modules/bundle/requireme.cjs
+const hello = 1;
+module.exports = { hello }
\ No newline at end of file

MODIFY
packages/compartment-mapper/test/test-bundle.js
packages/compartment-mapper/test/test-bundle.js
@@ -41,11 +41,18 @@ const expectedLog = [
green: '#0f0',
blue: '#00f',
},
+ {
+ default: {
+ zzz: 1,
+ fromMjs: 'foo',
+ },
+ fromMjs: 'foo',
+ zzz: 1,
+ },
];
test('bundles work', async t => {
const bundle = await makeBundle(read, fixture);
- // t.log(bundle);
const log = [];
const print = entry => {
log.push(entry);

feat(compartment-mapper): add bundle support for commonjs

Co-authored-by: naugtur <naugtur@gmail.com>
Co-authered-by: kumavis <kumavis@users.noreply.github.com>