Skip to content

Conversation

@kriskowal
Copy link
Member

Closes: #2897

Description

Adds support for wildcard pattern replacement in package.json exports and imports fields to the compartment-mapper. This enables patterns like:

{
  "exports": {
    "./x/*/y/*/z": "./src/x/*/y/*/z.js"
  },
  "imports": {
    "#internal/*": "./lib/*.js"
  }
}

The implementation uses a prefix-tree-based approach for efficient pattern matching, correctly implementing Node.js semantics where * matches any string within a single path segment (does not match across /).

Key changes:

New files:

  • src/pattern-replacement.js - Prefix-tree-based pattern matcher with PathPrefixTree and PathPrefixTreeNode classes
  • src/types/pattern-replacement.ts - Type definitions for pattern matching
  • test/pattern-replacement.test.js - 21 unit tests for pattern matching
  • test/export-patterns.test.js - 11 integration tests
  • test/fixtures-export-patterns/ - Test fixtures with wildcard patterns

Modified files:

  • src/infer-exports.js - Added inferExportsAliasesAndPatterns() to extract wildcard patterns from exports and imports fields
  • src/node-modules.js - Propagates patterns through the compartment graph
  • src/link.js - Runtime pattern resolution in makeModuleMapHook()
  • src/types/compartment-map-schema.ts - Added patterns field to PackageCompartmentDescriptor
  • src/types/node-modules.ts - Added patterns to Node type

Security Considerations

Patterns are resolved only within the same compartment, consistent with Node.js behavior. Pattern-matched module imports go through the same policy enforcement (enforcePolicyByModule) as other module resolutions. The prefix-tree approach avoids regex-based matching, eliminating potential ReDoS concerns.

Scaling Considerations

The prefix-tree data structure provides O(n) lookup where n is the number of path segments, which is efficient for typical module specifiers. Pattern matching is performed lazily at import time and results are cached via write-back to moduleDescriptors.

Documentation Considerations

Users can now use wildcard patterns in their package.json exports and imports fields when using compartment-mapper. The patterns follow Node.js subpath export semantics:

  • * matches any string within a single path segment
  • * does not match across / boundaries
  • Globstar (**) is explicitly not supported and throws an error
  • Pattern and replacement must have matching wildcard counts

Testing Considerations

  • 21 unit tests cover the prefix-tree implementation, wildcard matching, edge cases (globstar rejection, wildcard count mismatch), and various pattern formats
  • 11 integration tests via the scaffold verify the full pipeline: loadLocation, importLocation, mapNodeModules, makeArchive, parseArchive, writeArchive, loadArchive, importArchive
  • Tests verify that patterns work correctly through archiving (patterns are expanded to concrete module entries)

Compatibility Considerations

This is a purely additive feature. Existing packages without wildcard patterns are unaffected. The patterns field is only added to compartment descriptors when patterns exist (using conditional spread), so existing snapshot tests pass without modification.

Upgrade Considerations

No breaking changes. Packages can start using wildcard patterns in exports/imports immediately after upgrading to a version with this feature.

@kriskowal kriskowal requested a review from boneskull January 9, 2026 07:01
Copy link
Member

@boneskull boneskull left a comment

Choose a reason for hiding this comment

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

No real concerns here. Not even nits, really

* Package imports field for self-referencing subpath patterns.
* Keys must start with '#'.
*/
imports?: unknown;
Copy link
Member

Choose a reason for hiding this comment

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

Comment on lines +44 to +46
export type SubpathMapping =
| Array<[pattern: string, replacement: string]>
| Record<string, string>;
Copy link
Member

Choose a reason for hiding this comment

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

👍 for named tuple elements

* The imports field provides self-referencing subpath patterns that
* can be used to create private internal mappings.
*
* @param {string} _name - the name of the package (unused, but kept for consistency)
Copy link
Member

Choose a reason for hiding this comment

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

Consistency with what?

}
for (const [key, value] of entries(imports)) {
// imports keys must start with '#'
if (!key.startsWith('#')) {
Copy link
Member

Choose a reason for hiding this comment

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

(suggestion) warn

* @param {string} value
* @returns {boolean}
*/
const hasWildcard = (key, value) => key.includes('*') || value.includes('*');
Copy link
Member

Choose a reason for hiding this comment

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

Isn't it also important to know which of these two values contains the wildcard(s)?

* @param {string} segment
* @returns {boolean}
*/
const hasWildcard = segment => segment.includes(WILDCARD);
Copy link
Member

Choose a reason for hiding this comment

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

(could) Refactor and share with infer-exports.js

return patternSegment === specifierSegment ? '' : null;
}

const wildcardIndex = patternSegment.indexOf(WILDCARD);
Copy link
Member

Choose a reason for hiding this comment

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

(could) if we're able to assert here, I'd probably want to assert that wildcardIndex is a non-negative integer

`Globstar (**) patterns are not supported in pattern: "${pattern}"`,
);
}
if (replacement.includes(GLOBSTAR)) {
Copy link
Member

Choose a reason for hiding this comment

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

(could) hasGlobstar()

Comment on lines +13 to +14
t.is(node.value, null);
t.deepEqual(Object.keys(node.children), []);
Copy link
Member

Choose a reason for hiding this comment

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

(could) refactor to t.like(node, {value: null, children: []}) assuming it works the way I think it should

});

test('assertMatchingWildcardCount - throws for mismatched counts', t => {
const error = t.throws(() => assertMatchingWildcardCount('./*/a/*', './*'));
Copy link
Member

Choose a reason for hiding this comment

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

(complaining) t.throws() is weaksauce.

expect(
  () => assertMatchingWildcardCount('./*/a/*', './*'),
  'to throw',
  {
    message: expect.it(
      'to match', /wildcard count mismatch/i,
      'and', 'to match', /2/,
      'and', 'to match', /1/
    )
  }
);

(imperative) feel the power of BUPKIS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Compartment map support for subpath wildcard pattern replacement in exports (and imports)

3 participants