Skip to content
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,21 @@ core module:
}
```

Wildcard patterns are supported to match multiple modules, using `*` as a wildcard:

```jsonc
// .eslintrc
{
"settings": {
"import/core-modules": [
"electron",
"@my-monorepo/*", // matches @my-monorepo/package-a, @my-monorepo/package-b, etc.
"@my-*/*", // matches @my-org/package, @my-company/package, etc.
],
},
}
```

In Electron's specific case, there is a shared config named `electron`
that specifies this for you.

Expand Down
38 changes: 37 additions & 1 deletion src/core/importType.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,40 @@ function isInternalRegexMatch(name, settings) {
return internalScope && new RegExp(internalScope).test(name);
}

function isDangerousPattern(pattern) {
Copy link
Member

Choose a reason for hiding this comment

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

do we need this function at all now that it's just using minimatch?

// Block patterns that would match too broadly
if (pattern === '*') { return true; } // Matches everything
if (pattern === '**') { return true; } // Double wildcard
if (pattern === '*/*') { return true; } // Matches any scoped package
if (pattern === '.*') { return true; } // Regex-style wildcard
if (pattern.startsWith('.*')) { return true; } // Regex wildcards
if (pattern.endsWith('.*')) { return true; } // Regex wildcards

// Block patterns that are too short and broad
if (pattern.length <= 2 && pattern.includes('*')) { return true; }

// Block patterns with multiple wildcards that could be too broad
const wildcardCount = (pattern.match(/\*/g) || []).length;
if (wildcardCount > 1) {
// Allow valid scoped patterns like @namespace/* or @my-*/*, but block overly broad ones
if (!pattern.match(/^@[^/]+\/\*$/)) { return true; }
}

return false;
}

function matchesCoreModulePattern(name, pattern) {
// Prevent dangerous patterns that could match too broadly
if (isDangerousPattern(pattern)) {
return false;
}

const regexPattern = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*');
return new RegExp(`^${regexPattern}$`).test(name);
}

export function isAbsolute(name) {
return typeof name === 'string' && nodeIsAbsolute(name);
}
Expand All @@ -32,7 +66,9 @@ export function isBuiltIn(name, settings, path) {
if (path || !name) { return false; }
const base = baseModule(name);
const extras = settings && settings['import/core-modules'] || [];
return isCoreModule(base) || extras.indexOf(base) > -1;
return isCoreModule(base)
|| extras.indexOf(base) > -1
|| extras.some((pattern) => pattern.includes('*') && matchesCoreModulePattern(base, pattern));
}

const moduleRegExp = /^\w/;
Expand Down
88 changes: 88 additions & 0 deletions tests/src/core/importType.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,94 @@ describe('importType(name)', function () {
expect(importType('@org/foobar/some/path/to/resource.json', scopedContext)).to.equal('builtin');
});

it("should return 'builtin' for wildcard patterns in core modules", function () {
// Test basic wildcard patterns
const wildcardContext = testContext({ 'import/core-modules': ['@my-monorepo/*'] });
expect(importType('@my-monorepo/package-a', wildcardContext)).to.equal('builtin');
expect(importType('@my-monorepo/package-b', wildcardContext)).to.equal('builtin');
expect(importType('@my-monorepo/some-long-package-name', wildcardContext)).to.equal('builtin');

// Test that non-matching patterns return external
expect(importType('@other-org/package', wildcardContext)).to.equal('external');
expect(importType('regular-package', wildcardContext)).to.equal('external');
expect(importType('@my-monorepo-but-not-scoped/package', wildcardContext)).to.equal('external');
});

it("should return 'builtin' for wildcard patterns with multiple wildcards", function () {
const multiWildcardContext = testContext({ 'import/core-modules': ['@my-*/*'] });
expect(importType('@my-org/package', multiWildcardContext)).to.equal('builtin');
expect(importType('@my-company/package', multiWildcardContext)).to.equal('builtin');
expect(importType('@my-test/package', multiWildcardContext)).to.equal('builtin');

// Should not match different patterns
expect(importType('@other-org/package', multiWildcardContext)).to.equal('external');
expect(importType('my-org/package', multiWildcardContext)).to.equal('external');
});

it("should return 'builtin' for resources inside wildcard core modules", function () {
const wildcardContext = testContext({ 'import/core-modules': ['@my-monorepo/*'] });
expect(importType('@my-monorepo/package-a/some/path/to/resource.json', wildcardContext)).to.equal('builtin');
expect(importType('@my-monorepo/package-b/nested/module', wildcardContext)).to.equal('builtin');
});

it('should support mixing exact matches and wildcards in core modules', function () {
const mixedContext = testContext({ 'import/core-modules': ['electron', '@my-monorepo/*', '@specific/package'] });

// Exact matches should work
expect(importType('electron', mixedContext)).to.equal('builtin');
expect(importType('@specific/package', mixedContext)).to.equal('builtin');

// Wildcard matches should work
expect(importType('@my-monorepo/any-package', mixedContext)).to.equal('builtin');

// Non-matches should be external
expect(importType('@other/package', mixedContext)).to.equal('external');
});

it('should handle dangerous wildcard patterns safely', function () {
// Test various dangerous patterns that should be blocked
const dangerousPatterns = [
'*', // Bare wildcard
'**', // Double wildcard
'*/*', // Any scoped package
'.*', // Regex wildcard
'.*foo', // Regex prefix
'foo.*', // Regex suffix
'a*', // Too short and broad
'*a', // Too short and broad
'*foo*', // Multiple wildcards (too broad)
'foo*bar*', // Multiple wildcards (too broad)
'*/*/*', // Triple wildcards
];

dangerousPatterns.forEach((pattern) => {
const context = testContext({ 'import/core-modules': [pattern] });
expect(importType('react', context)).to.equal('external', `Pattern "${pattern}" should not match anything`);
expect(importType('lodash', context)).to.equal('external', `Pattern "${pattern}" should not match anything`);
expect(importType('@babel/core', context)).to.equal('external', `Pattern "${pattern}" should not match anything`);
});

// Test that valid patterns still work
const validPatterns = [
'@my-org/*', // Valid scoped wildcard
'my-prefix-*', // Valid prefix wildcard
'@namespace/prefix-*', // Valid scoped prefix wildcard
'electron', // Exact match (no wildcard)
];

validPatterns.forEach((pattern) => {
const context = testContext({ 'import/core-modules': [pattern] });
// Should not break the system - external packages should still be external
expect(importType('totally-different-package', context)).to.equal('external', `Pattern "${pattern}" should not break normal operation`);
});

// Test specific valid matches
const validContext = testContext({ 'import/core-modules': ['@my-org/*', 'my-prefix-*'] });
expect(importType('@my-org/package', validContext)).to.equal('builtin');
expect(importType('my-prefix-tool', validContext)).to.equal('builtin');
expect(importType('react', validContext)).to.equal('external');
});

it("should return 'external' for module from 'node_modules' with default config", function () {
expect(importType('resolve', context)).to.equal('external');
});
Expand Down
17 changes: 17 additions & 0 deletions tests/src/rules/no-extraneous-dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,23 @@ ruleTester.run('no-extraneous-dependencies', rule, {
code: 'import "@generated/bar/and/sub/path"',
settings: { 'import/core-modules': ['@generated/bar'] },
}),
// Test wildcard patterns in core-modules
test({
code: 'import "@my-monorepo/package-a"',
settings: { 'import/core-modules': ['@my-monorepo/*'] },
}),
test({
code: 'import "@my-monorepo/package-b/nested/module"',
settings: { 'import/core-modules': ['@my-monorepo/*'] },
}),
test({
code: 'import "@my-org/any-package"',
settings: { 'import/core-modules': ['@my-*/*'] },
}),
test({
code: 'import "@namespace/any-package"',
settings: { 'import/core-modules': ['@namespace/*', 'specific-module'] },
}),
// check if "rxjs" dependency declaration fix the "rxjs/operators subpackage
test({
code: 'import "rxjs/operators"',
Expand Down
Loading