Skip to content

feat(compartment-mapper): support mapping of undiscoverable packages #2856

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

boneskull
Copy link
Member

@boneskull boneskull commented Jun 12, 2025

Description

This adds the additionalPackageLocations and additionalPackageDetails options to mapNodeModules(). The former is a mapping of module location to one or more additional module locations. The latter is a data structure mapNodeModules will populate, which can then be provided to captureFromMap (which is in turn provided to makeImportHookMaker().

This is intended to be used when dynamic requires prevent mapNodeModules & captureFromMap from otherwise creating a complete CompartmentMapDescriptor due to its ignorance of what would be dynamically required. Archival is not a use-case.

Also:

  • Fixed signature of chooseModuleDescriptor
  • Tweaked ESLint config to ignore my new fixture
  • Add options type for translateGraph
  • Fixed deprecation notice for compartmentMapForNodeModules; it was previously on the options object, but it should only be on the export.
  • graphPackages now accepts an optional Graph object to be able to re-use the same graph between runs

Questions for Reviewers

  • Naming is hard and I was not sure what to name the new options. The AdditionalPackageDetails type looks like the existing PackageDetails type (with a new field) thus influencing the naming—but they are otherwise unrelated and because of this I did not extend PackageDetails.
  • I am monkeying with imports in a RecordModuleDescriptor object in order to direct SES' module loading. Is there a better way? The things I stuff in imports are also extremely dubious.

Security Considerations

n/a

These changes do not meddle with systems loading untrusted code, afaik.

Scaling Considerations

Use of the new option is not free.

Documentation Considerations

n/a

Testing Considerations

I did add some tests, but could probably use more.

Compatibility Considerations

No.

Upgrade Considerations

This warrants an entry in NEWS.md since it is part of a public API.

This adds the `additionalPackageLocations` and `additionalPackageDetails` options to `mapNodeModules`. The former is a mapping of module location to one or more additional module locations. The latter is a data structure `mapNodeModules` will populate, which can then be provided to `captureFromMap` (which is in turn provided to `makeImportHookMaker()`.

This is intended to be used when dynamic requires prevent `mapNodeModules` & `captureFromMap` from otherwise creating a complete `CompartmentMapDescriptor` due to its ignorance of what would be dynamically required.  Archival is not a use-case.

Also:

- Fixed signature of `chooseModuleDescriptor`
- Tweaked ESLint config to ignore my new fixture
- Add options type for `translateGraph`
- Fixed deprecation notice for `compartmentMapForNodeModules`; it was previously on the options object, but it should only be on the export.
- `graphPackages` now accepts an optional `Graph` object to be able to re-use the same graph between runs
@boneskull boneskull self-assigned this Jun 12, 2025
@boneskull boneskull added the enhancement New feature or request label Jun 12, 2025
@boneskull boneskull requested review from kriskowal and naugtur June 12, 2025 21:36
Comment on lines +631 to +634
// XXX: this "works" but that's probably a conincidence!
record.record?.imports?.push(
`${compartmentDescriptors[additionalPackageLocation].name}${additionalPackageModuleSpecifier.substring(1)}`,
);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⬆️ 🚨 ⬆️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm expecting an explosion of edge cases here, starting with there being a bunch of record types and only one (or two?) have the structure of record.record so it's a little specific to what you've been testing with.
Unless no other record shape needs to be affected by this.

I don't like the concept of stuffing imports on the records. It's obviously working if added early enough, but there might be a more gentle way.


I'd rather include the information necessary for this to work in the compartmentMap itself. What's preventing that?

  const compartmentMap = await mapNodeModules(readPowers, entryModuleLocation, {
    additionalModuleLocations,
    // ~~additionalPackageDetails~~
  });

If we managed to do that, the step to manually add imports here could end up being unnecessary.


A single map of referrer-target should be enough to feed graphPackages with information to add missing links to the list it gets from packageDescriptor
Calling load on each of the targets in the same place where we call load on attenuators would suffice to keep them retained.

I'm not 100% sure the links are even necessary. If we had multiple entries, made graphPackages accept multiple (or call it many times if that works) and then proceed to load each of them, it should be enough to reach them through importNow flow.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a more precise wording of what I mean in a language that works better than English:
#2864

`${compartmentDescriptors[additionalPackageLocation].name}${additionalPackageModuleSpecifier.substring(1)}`,
);
} else {
throw new Error(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the only way for this to happen would be to provide a broken CompartmentMapDescriptor in the first place. IDK if it's worth worrying about.

packageLocation,
packageDescriptor: { name },
} of moduleDetails) {
node.dependencyLocations[name] ??= packageLocation;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems to be sufficient for associating a Node with another Node, but maybe there's a safer way to do so?

Comment on lines +1098 to +1099
moduleDescriptor.retained = true;
compartmentMap.compartments[packageLocation].retained = true;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure these are actually needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manually setting retained should not be necessary and if it is, it suggests a flaw in the design.
Causing them to be retained should make other things easier too.

@@ -1075,12 +1142,54 @@ export const mapNodeModules = async (
parseLocatedJson(packageDescriptorText, packageDescriptorLocation)
);

return compartmentMapForNodeModules(
await Promise.all(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afaict there is no memoization happening here that would make it so I shouldn't just spray promises everywhere

Comment on lines +98 to +107
const entryModuleLocation = new URL(
'fixtures-additional-modules/node_modules/goofy/index.js',
import.meta.url,
).href;

const additionalModuleLocations = {
[entryModuleLocation]: [
new URL('fixtures-additional-modules/config.js', import.meta.url).href,
],
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fixture here mimics something like webpack. we have a tool (goofy) and that tool dynamically loads a config file (config.js) from our application root.

Copy link
Member

@naugtur naugtur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review in progress, but I think there might be a less (opinion) invasive way to serve the same need.

That being said, I want to encourage others to review because this exists and my hypothetical other way doesn't (at least until I try)

Comment on lines +631 to +634
// XXX: this "works" but that's probably a conincidence!
record.record?.imports?.push(
`${compartmentDescriptors[additionalPackageLocation].name}${additionalPackageModuleSpecifier.substring(1)}`,
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm expecting an explosion of edge cases here, starting with there being a bunch of record types and only one (or two?) have the structure of record.record so it's a little specific to what you've been testing with.
Unless no other record shape needs to be affected by this.

I don't like the concept of stuffing imports on the records. It's obviously working if added early enough, but there might be a more gentle way.


I'd rather include the information necessary for this to work in the compartmentMap itself. What's preventing that?

  const compartmentMap = await mapNodeModules(readPowers, entryModuleLocation, {
    additionalModuleLocations,
    // ~~additionalPackageDetails~~
  });

If we managed to do that, the step to manually add imports here could end up being unnecessary.


A single map of referrer-target should be enough to feed graphPackages with information to add missing links to the list it gets from packageDescriptor
Calling load on each of the targets in the same place where we call load on attenuators would suffice to keep them retained.

I'm not 100% sure the links are even necessary. If we had multiple entries, made graphPackages accept multiple (or call it many times if that works) and then proceed to load each of them, it should be enough to reach them through importNow flow.

Comment on lines +1098 to +1099
moduleDescriptor.retained = true;
compartmentMap.compartments[packageLocation].retained = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manually setting retained should not be necessary and if it is, it suggests a flaw in the design.
Causing them to be retained should make other things easier too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants