Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions __tests__/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,38 @@ describe('commitlintPluginSelectiveScope', () => {
).toBe(true)
})

// @commitlint/load uses es-toolkit merge, which turns config `[null]` into `[undefined]`.
it('should treat undefined in the allowed list as a no-scope marker (commitlint load)', () => {
expect(
commitlintPluginSelectiveScopeResolverWrapped(
{ scope: null, type: 'ci' },
{ ci: [undefined] }
)[0]
).toBe(true)

expect(
commitlintPluginSelectiveScopeResolverWrapped(
{ scope: undefined, type: 'ci' },
{ ci: [undefined] }
)[0]
).toBe(true)

expect(
commitlintPluginSelectiveScopeResolverWrapped(
{ scope: null, type: 'ci' },
{ ci: [undefined, 'codebuild'] }
)[0]
).toBe(true)

const [ok, message] = commitlintPluginSelectiveScopeResolverWrapped(
{ scope: 'github', type: 'ci' },
{ ci: [undefined, 'codebuild'] }
)
expect(ok).toBe(false)
expect(message).not.toContain('"undefined"')
expect(message).toContain('"codebuild"')
})

it('should match scope if provided', () => {
expect(
commitlintPluginSelectiveScopeResolverWrapped(
Expand Down
37 changes: 26 additions & 11 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ function normalizeScope(scope) {
return scope
}

// @commitlint/load merges config with es-toolkit, which turns array `null`
// entries into `undefined`. Treat both as "no scope" markers.
function isNoScopeMarker(value) {
return value == null
}

function formatAllowedScopes(allowedScopes) {
return allowedScopes
.filter(s => !isNoScopeMarker(s))
.map(s => `"${s}"`)
.join(', ')
}

module.exports = {
rules: {
'selective-scope': (ctx, applicable, rule) => {
Expand All @@ -24,7 +37,7 @@ module.exports = {
}

if (Array.isArray(allowedScopes)) {
// If the type maps to an empty array in the rule config, scope it not allowed
// If the type maps to an empty array in the rule config, scope is not allowed
if (allowedScopes.length === 0) {
if (scope != null) {
return [
Expand All @@ -36,39 +49,41 @@ module.exports = {
return [true]
}

// Otherwise attempt to match against either null, a string literal, or a RegExp
// Otherwise attempt to match against either null/undefined, a string literal, or a RegExp
if (
allowedScopes.findIndex(s => {
if (isNoScopeMarker(s)) {
return scope == null
}

if (
typeof scope === 'string' &&
Object.prototype.toString.call(s) === '[object RegExp]'
) {
return scope.match(s)
}

// Equality comparison works for both strings and null
return s === scope
}) !== -1
) {
return [true]
} else if (allowedScopes.includes(null)) {
} else if (allowedScopes.some(isNoScopeMarker)) {
return [
false,
`commit message with type "${
ctx.type
}" may specify a scope, but if specified, it must be one of the following: ${allowedScopes
.filter(s => s !== null)
.map(s => `"${s}"`)
.join(', ')}`
}" may specify a scope, but if specified, it must be one of the following: ${formatAllowedScopes(
allowedScopes
)}`
]
} else {
return [
false,
`commit message with type "${
ctx.type
}" must specify one of the following scopes: ${allowedScopes
.map(s => `"${s}"`)
.join(', ')}`
}" must specify one of the following scopes: ${formatAllowedScopes(
allowedScopes
)}`
]
}
}
Expand Down