From c38c16df69f1e293ef512aa0af9680810d32b18e Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Tue, 17 Dec 2024 16:45:02 -0800 Subject: [PATCH] fix(compartment-mapper/node): support anonymous entrypoints An "anonymous" entrypoint is one where the closest `package.json` does not contain a `name` field. It is not unusual for CJS packages to create a subdirectory containing `package.json` like this: ```json {"type": "module"} ``` The effect is that any `.js` file in this subdir will be treated as an ECMAScript module by Node.js. A problem arises when a module in this subdir is provided as an entrypoint to `mapNodeModules()`. In this case, the "root" package descriptor _has no name_. Ultimately, the resulting shape of the `CompartmentMapDescriptor` fails `assertCompartmentMap()`, presumably due to an `undefined` value (or `"undefined"` string!) where one was not expected. To avoid the failure, we can assign a string value to the root package descriptor's `name` field. This is very much a corner case and was discovered by running arbitrary packages through `mapNodeModules()` (specifically, `@babel/runtime`). - Also added a test for this - Also added a broad/shallow test case for `mapNodeModules()` since one did not exist * * * Questions at time of commit: - Does the name need to be more unique to avoid collisions with other packages in the dependency tree? - Severe enough to pull in `node:crypto`? - Can there ever be more than one "anonymous" package? --- .../compartment-mapper/src/node-modules.js | 14 +++++ .../node_modules/unnamed/incognito/index.js | 1 + .../unnamed/incognito/package.json | 3 + .../node_modules/unnamed/package.json | 11 ++++ .../test/node-modules.test.js | 58 +++++++++++++++++++ 5 files changed, 87 insertions(+) create mode 100644 packages/compartment-mapper/test/fixtures-anonymous/node_modules/unnamed/incognito/index.js create mode 100644 packages/compartment-mapper/test/fixtures-anonymous/node_modules/unnamed/incognito/package.json create mode 100644 packages/compartment-mapper/test/fixtures-anonymous/node_modules/unnamed/package.json create mode 100644 packages/compartment-mapper/test/node-modules.test.js diff --git a/packages/compartment-mapper/src/node-modules.js b/packages/compartment-mapper/src/node-modules.js index ef1f183d00..7a9f7c27c1 100644 --- a/packages/compartment-mapper/src/node-modules.js +++ b/packages/compartment-mapper/src/node-modules.js @@ -547,6 +547,11 @@ const gatherDependency = async ( ); }; +/** + * The name used by a root entry compartment that has no `name` in its package descriptor. + */ +export const ANONYMOUS_COMPARTMENT = ''; + /** * graphPackages returns a graph whose keys are nominally URLs, one per * package, with values that are label: (an informative Compartment name, built @@ -621,6 +626,15 @@ const graphPackages = async ( }; } + /** + * If the entry package descriptor has no `name`, we need to put _something_ + * in there to pass compartment map validation (go find + * `assertCompartmentMap()`). + */ + if (!packageDescriptor.name) { + packageDescriptor.name = ANONYMOUS_COMPARTMENT; + } + const graph = create(null); await graphPackage( packageDescriptor.name, diff --git a/packages/compartment-mapper/test/fixtures-anonymous/node_modules/unnamed/incognito/index.js b/packages/compartment-mapper/test/fixtures-anonymous/node_modules/unnamed/incognito/index.js new file mode 100644 index 0000000000..32802f71ae --- /dev/null +++ b/packages/compartment-mapper/test/fixtures-anonymous/node_modules/unnamed/incognito/index.js @@ -0,0 +1 @@ +export default 'doctor emilio lizardo'; diff --git a/packages/compartment-mapper/test/fixtures-anonymous/node_modules/unnamed/incognito/package.json b/packages/compartment-mapper/test/fixtures-anonymous/node_modules/unnamed/incognito/package.json new file mode 100644 index 0000000000..aead43de36 --- /dev/null +++ b/packages/compartment-mapper/test/fixtures-anonymous/node_modules/unnamed/incognito/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/packages/compartment-mapper/test/fixtures-anonymous/node_modules/unnamed/package.json b/packages/compartment-mapper/test/fixtures-anonymous/node_modules/unnamed/package.json new file mode 100644 index 0000000000..7d092e9795 --- /dev/null +++ b/packages/compartment-mapper/test/fixtures-anonymous/node_modules/unnamed/package.json @@ -0,0 +1,11 @@ +{ + "name": "unnamed", + "version": "1.0.0", + "exports": { + "./incognito": "./incognito/index.js" + }, + "type": "commonjs", + "scripts": { + "preinstall": "echo DO NOT INSTALL TEST FIXTURES; exit -1" + } +} diff --git a/packages/compartment-mapper/test/node-modules.test.js b/packages/compartment-mapper/test/node-modules.test.js new file mode 100644 index 0000000000..4af66b28f2 --- /dev/null +++ b/packages/compartment-mapper/test/node-modules.test.js @@ -0,0 +1,58 @@ +import 'ses'; +import fs from 'node:fs'; +import url from 'node:url'; +import test from 'ava'; +import { mapNodeModules } from '../src/node-modules.js'; +import { makeReadPowers } from '../src/node-powers.js'; +import { ANONYMOUS_COMPARTMENT } from '../src/node-modules.js'; + +const { keys, entries, values } = Object; + +test('mapNodeModules() should fulfill with a denormalized CompartmentMapDescriptor', async t => { + t.plan(4); + + const readPowers = makeReadPowers({ fs, url }); + const moduleLocation = `${new URL( + 'fixtures-0/node_modules/bundle/main.js', + import.meta.url, + )}`; + + const { compartments, entry } = await mapNodeModules( + readPowers, + moduleLocation, + ); + + t.deepEqual( + values(compartments) + .map(({ name }) => name) + .sort(), + ['bundle', 'bundle-dep'], + ); + + t.true(keys(compartments).every(name => name.startsWith('file://'))); + + t.is(compartments[entry.compartment].name, 'bundle'); + + t.deepEqual(keys(compartments[entry.compartment].modules).sort(), [ + '.', + 'bundle', + 'bundle-dep', + ]); +}); + +test(`mapNodeModules() should assign a package name when the entry point's package descriptor lacks a "name" field`, async t => { + t.plan(1); + + const readPowers = makeReadPowers({ fs, url }); + const moduleLocation = `${new URL( + 'fixtures-anonymous/node_modules/unnamed/incognito/index.js', + import.meta.url, + )}`; + + const { compartments } = await mapNodeModules(readPowers, moduleLocation); + + t.deepEqual( + values(compartments).map(({ name }) => name), + [ANONYMOUS_COMPARTMENT], + ); +});