├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc.js ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ └── plugin.test.js ├── commitlint.config.js ├── index.js ├── jest.config.js ├── package-lock.json ├── package.json └── release.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /node_modules 3 | package.json 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true, 6 | jest: true 7 | }, 8 | plugins: ['prettier'], 9 | extends: [ 10 | 'standard', 11 | // resolve conflicts between prettier and eslint 12 | 'plugin:prettier/recommended' 13 | ], 14 | parserOptions: { 15 | ecmaVersion: 12 16 | }, 17 | rules: {} 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor 2 | .vscode/ 3 | 4 | # Secure 5 | .coveralls.yml 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Snowpack dependency directory (https://snowpack.dev/) 51 | web_modules/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | 81 | # parcel-bundler cache (https://parceljs.org/) 82 | .cache 83 | .parcel-cache 84 | 85 | # Next.js build output 86 | .next 87 | out 88 | 89 | # Nuxt.js build / generate output 90 | .nuxt 91 | dist 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and not Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | 102 | # Serverless directories 103 | .serverless/ 104 | 105 | # FuseBox cache 106 | .fusebox/ 107 | 108 | # DynamoDB Local files 109 | .dynamodb/ 110 | 111 | # TernJS port file 112 | .tern-port 113 | 114 | # Stores VSCode versions used for testing VSCode extensions 115 | .vscode-test 116 | 117 | # yarn v2 118 | .yarn/cache 119 | .yarn/unplugged 120 | .yarn/build-state.yml 121 | .yarn/install-state.gz 122 | .pnp.* 123 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run pre-commit 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | singleQuote: true, 4 | trailingComma: "none", 5 | arrowParens: "avoid", 6 | semi: false, 7 | }; 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 17 4 | before_script: 5 | - npm prune 6 | jobs: 7 | include: 8 | - stage: test 9 | script: 10 | - echo "Linting ..." && npm run lint 11 | - echo "Code Coverage and Test ..." && npm run test:coverage 12 | after-success: 13 | - echo "Semantic Release Deployment .." 14 | - npm run semantic-release 15 | branches: 16 | only: 17 | - master 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.1](https://github.com/ridvanaltun/commitlint-plugin-selective-scope/compare/v1.0.0...v1.0.1) (2022-12-22) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * correctly check for RegExp ([af6d426](https://github.com/ridvanaltun/commitlint-plugin-selective-scope/commit/af6d426cd08a3c7bbbc0a47f4e8ace13210ad8f0)) 7 | 8 | # 1.0.0 (2021-09-30) 9 | 10 | 11 | ### Features 12 | 13 | * initial version ([bcdf5c1](https://github.com/ridvanaltun/commitlint-plugin-selective-scope/commit/bcdf5c187dacd1362e808139a28c8a0a68a23bed)) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ridvan Altun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | [![npm version](https://img.shields.io/npm/v/commitlint-plugin-selective-scope.svg)](https://npmjs.com/package/commitlint-plugin-selective-scope) 4 | [![npm downloads](https://img.shields.io/npm/dt/commitlint-plugin-selective-scope.svg)](https://npmjs.com/package/commitlint-plugin-selective-scope) 5 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 6 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 7 | [![Build Status](https://travis-ci.com/ridvanaltun/commitlint-plugin-selective-scope.svg?branch=master)](https://travis-ci.com/ridvanaltun/commitlint-plugin-selective-scope) 8 | [![Coverage Status](https://coveralls.io/repos/github/ridvanaltun/commitlint-plugin-selective-scope/badge.svg?branch=master)](https://coveralls.io/github/ridvanaltun/commitlint-plugin-selective-scope?branch=master) 9 | [![license](https://img.shields.io/npm/l/commitlint-plugin-selective-scope.svg)](https://github.com/ridvanaltun/commitlint-plugin-selective-scope/blob/master/LICENSE) 10 | 11 | > Limit scopes per type with regexp and plain text. 12 | 13 | ## Installation 14 | 15 | ```bash 16 | npm i commitlint-plugin-selective-scope --save-dev 17 | ``` 18 | 19 | ## Usage 20 | 21 | - If a type does not appear in the rule, do not enforce scope 22 | - If a type appears in the rule with an empty array, do not allow scope 23 | - If a type appears in the rule with an non-empty array, only allow the values in the array for scope. 24 | - If the array includes null, the scope is optional. 25 | 26 | ## Example 27 | 28 | ```javascript 29 | module.exports = { 30 | plugins: ['selective-scope'], 31 | rules: { 32 | 'selective-scope': [ 33 | 2, 34 | 'always', 35 | { 36 | feat: [/^frontend\/[^\/]+$/, 'backend'], 37 | perf: [], 38 | ci: [null, 'codebuild', 'jenkins'] 39 | } 40 | ] 41 | } 42 | } 43 | ``` 44 | 45 | **In the above rules configuration,** 46 | 47 | - if the type is `feat`, the scope must be either match the regex `/frontend\/[^\/]+/` or be `backend` 48 | - if the type if `chore`, the scope is optional and can be anything 49 | - if the type is `perf`, a scope is not allowed 50 | - if the type is `ci`, the scope must be `codebuild` or `jenkins` if present, but is not required 51 | -------------------------------------------------------------------------------- /__tests__/plugin.test.js: -------------------------------------------------------------------------------- 1 | const commitlintPluginSelectiveScope = require('../index') 2 | const commitlintPluginSelectiveScopeResolver = 3 | commitlintPluginSelectiveScope.rules['selective-scope'] 4 | // Wrap the resolver so we don't have to pass in "always" 5 | const commitlintPluginSelectiveScopeResolverWrapped = (ctx, rules) => 6 | commitlintPluginSelectiveScopeResolver(ctx, 'always', rules) 7 | 8 | const TEST_RULES = { 9 | feat: [/^frontend\/[^/]+$/, 'backend'], 10 | perf: [], 11 | ci: [null, 'codebuild', 'jenkins'] 12 | } 13 | 14 | describe('commitlintPluginSelectiveScope', () => { 15 | it('should return a valid config', () => { 16 | expect(commitlintPluginSelectiveScope).toHaveProperty('rules') 17 | expect( 18 | Object.keys(commitlintPluginSelectiveScope.rules).length 19 | ).toBeGreaterThan(0) 20 | }) 21 | 22 | it('should not enforce scope if the type does not appear in the rules', () => { 23 | expect( 24 | commitlintPluginSelectiveScopeResolverWrapped( 25 | { scope: 'frontend/web', type: 'fix' }, 26 | TEST_RULES 27 | )[0] 28 | ).toBe(true) 29 | 30 | expect( 31 | commitlintPluginSelectiveScopeResolverWrapped( 32 | { scope: 'anything', type: 'fix' }, 33 | TEST_RULES 34 | )[0] 35 | ).toBe(true) 36 | 37 | expect( 38 | commitlintPluginSelectiveScopeResolverWrapped( 39 | { scope: null, type: 'fix' }, 40 | TEST_RULES 41 | )[0] 42 | ).toBe(true) 43 | }) 44 | 45 | it('should not allow scope if the type appears in the rules with an empty array', () => { 46 | expect( 47 | commitlintPluginSelectiveScopeResolverWrapped( 48 | { scope: null, type: 'perf' }, 49 | TEST_RULES 50 | )[0] 51 | ).toBe(true) 52 | 53 | expect( 54 | commitlintPluginSelectiveScopeResolverWrapped( 55 | { scope: 'something', type: 'perf' }, 56 | TEST_RULES 57 | )[0] 58 | ).toBe(false) 59 | }) 60 | 61 | describe('should only allow scopes defined if the type appears in the rule with a non-empty array', () => { 62 | it('should properly match a string literal', () => { 63 | expect( 64 | commitlintPluginSelectiveScopeResolverWrapped( 65 | { scope: 'backend', type: 'feat' }, 66 | TEST_RULES 67 | )[0] 68 | ).toBe(true) 69 | 70 | expect( 71 | commitlintPluginSelectiveScopeResolverWrapped( 72 | { scope: 'test', type: 'feat' }, 73 | TEST_RULES 74 | )[0] 75 | ).toBe(false) 76 | }) 77 | 78 | it('should properly match a RegExp', () => { 79 | expect( 80 | commitlintPluginSelectiveScopeResolverWrapped( 81 | { scope: 'frontend/web', type: 'feat' }, 82 | TEST_RULES 83 | )[0] 84 | ).toBe(true) 85 | 86 | expect( 87 | commitlintPluginSelectiveScopeResolverWrapped( 88 | { scope: 'frontend', type: 'feat' }, 89 | TEST_RULES 90 | )[0] 91 | ).toBe(false) 92 | }) 93 | }) 94 | 95 | describe('optional scope', () => { 96 | it('should allow scope to be optional if the type appears in the rules with a null in the array', () => { 97 | expect( 98 | commitlintPluginSelectiveScopeResolverWrapped( 99 | { scope: null, type: 'ci' }, 100 | TEST_RULES 101 | )[0] 102 | ).toBe(true) 103 | }) 104 | 105 | it('should match scope if provided', () => { 106 | expect( 107 | commitlintPluginSelectiveScopeResolverWrapped( 108 | { scope: 'codebuild', type: 'ci' }, 109 | TEST_RULES 110 | )[0] 111 | ).toBe(true) 112 | 113 | expect( 114 | commitlintPluginSelectiveScopeResolverWrapped( 115 | { scope: 'jenkins', type: 'ci' }, 116 | TEST_RULES 117 | )[0] 118 | ).toBe(true) 119 | 120 | expect( 121 | commitlintPluginSelectiveScopeResolverWrapped( 122 | { scope: 'github', type: 'ci' }, 123 | TEST_RULES 124 | )[0] 125 | ).toBe(false) 126 | }) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'body-max-line-length': [1, 'always', 100] 5 | }, 6 | helpUrl: ` 7 | Commit messages must follow conventional commit format: 8 | https://www.conventionalcommits.org/en/v1.0.0/#summary 9 | type(optional-scope): subject 10 | 11 | [optional body] 12 | * To bypass pre-commit hooks run 'git commit --no-verify' 13 | >>> Use "npm run commit" for interactive prompt. <<< 14 | ` 15 | } 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'selective-scope': (ctx, applicable, rule) => { 4 | if (applicable === 'never') { 5 | return [false, 'the "allowed-scopes" rule does not support "never"'] 6 | } 7 | 8 | const allowedScopes = rule[ctx.type] 9 | 10 | // If the type does not appear in the rule config, allow any scope or no scope 11 | if (allowedScopes === undefined) { 12 | return [true] 13 | } 14 | 15 | if (Array.isArray(allowedScopes)) { 16 | // If the type maps to an empty array in the rule config, scope it not allowed 17 | if (allowedScopes.length === 0) { 18 | if (ctx.scope != null) { 19 | return [ 20 | false, 21 | `commit messages with type "${ctx.type}" must not specify a scope` 22 | ] 23 | } 24 | 25 | return [true] 26 | } 27 | 28 | // Otherwise attempt to match against either null, a string literal, or a RegExp 29 | if ( 30 | allowedScopes.findIndex(s => { 31 | if ( 32 | typeof ctx.scope === 'string' && 33 | Object.prototype.toString.call(s) === '[object RegExp]' 34 | ) { 35 | return ctx.scope.match(s) 36 | } 37 | 38 | // Equality comparison works for both strings and null 39 | return s === ctx.scope 40 | }) !== -1 41 | ) { 42 | return [true] 43 | } else if (allowedScopes.includes(null)) { 44 | return [ 45 | false, 46 | `commit message with type "${ 47 | ctx.type 48 | }" may specify a scope, but if specified, it must be one of the following: ${allowedScopes 49 | .filter(s => s !== null) 50 | .map(s => `"${s}"`) 51 | .join(', ')}` 52 | ] 53 | } else { 54 | return [ 55 | false, 56 | `commit message with type "${ 57 | ctx.type 58 | }" must specify one of the following scopes: ${allowedScopes 59 | .map(s => `"${s}"`) 60 | .join(', ')}` 61 | ] 62 | } 63 | } 64 | 65 | return [ 66 | false, 67 | `invalid rule entry: { ${ctx.type}: ${JSON.stringify(allowedScopes)} }` 68 | ] 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | roots: ['__tests__/'] 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commitlint-plugin-selective-scope", 3 | "version": "1.0.1", 4 | "description": "Limit scopes per type with regexp and plain text", 5 | "main": "index.js", 6 | "scripts": { 7 | "commit": "git-cz", 8 | "lint": "eslint .", 9 | "lint:fix": "npm run lint -- --fix", 10 | "lint:active": "eslint-nibble .", 11 | "test": "jest --config ./jest.config.js", 12 | "test:coverage": "npm test -- --coverage && cat ./coverage/lcov.info | coveralls", 13 | "semantic-release": "semantic-release", 14 | "prepare": "husky install", 15 | "pre-commit": "lint-staged" 16 | }, 17 | "keywords": [ 18 | "commitlint", 19 | "commitlint-plugin", 20 | "commitlint-plugin-selective-scope" 21 | ], 22 | "author": { 23 | "name": "Rıdvan Altun", 24 | "email": "ridvanaltun@outlook.com", 25 | "url": "https://ridvanaltun.github.io/" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/ridvanaltun/commitlint-plugin-selective-scope.git" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/ridvanaltun/commitlint-plugin-selective-scope/issues" 33 | }, 34 | "homepage": "https://github.com/ridvanaltun/commitlint-plugin-selective-scope#readme", 35 | "license": "MIT", 36 | "devDependencies": { 37 | "@commitlint/cli": "^13.2.0", 38 | "@commitlint/config-conventional": "^13.2.0", 39 | "@commitlint/cz-commitlint": "^13.2.0", 40 | "@semantic-release/changelog": "^6.0.0", 41 | "@semantic-release/git": "^10.0.0", 42 | "commitizen": "^4.2.4", 43 | "coveralls": "^3.1.1", 44 | "eslint": "^7.32.0", 45 | "eslint-config-prettier": "^8.3.0", 46 | "eslint-config-standard": "^16.0.3", 47 | "eslint-nibble": "^7.0.0", 48 | "eslint-plugin-import": "^2.24.2", 49 | "eslint-plugin-node": "^11.1.0", 50 | "eslint-plugin-prettier": "^4.0.0", 51 | "eslint-plugin-promise": "^5.1.0", 52 | "husky": "^7.0.2", 53 | "jest": "^27.2.4", 54 | "lint-staged": "^11.1.2", 55 | "prettier": "^2.4.1", 56 | "semantic-release": "^18.0.0" 57 | }, 58 | "lint-staged": { 59 | "*.js": [ 60 | "eslint --fix" 61 | ] 62 | }, 63 | "config": { 64 | "commitizen": { 65 | "path": "@commitlint/cz-commitlint" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-template-curly-in-string */ 2 | 3 | module.exports = { 4 | branches: ['master'], 5 | plugins: [ 6 | '@semantic-release/commit-analyzer', 7 | '@semantic-release/release-notes-generator', 8 | [ 9 | '@semantic-release/changelog', 10 | { 11 | changelogFile: 'CHANGELOG.md' 12 | } 13 | ], 14 | '@semantic-release/npm', 15 | '@semantic-release/github', 16 | [ 17 | '@semantic-release/git', 18 | { 19 | assets: ['package.json', 'package-lock.json', 'CHANGELOG.md'], 20 | message: 21 | 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}' 22 | } 23 | ] 24 | ] 25 | } 26 | --------------------------------------------------------------------------------