Skip to content

parserPreset is overwritten when extending multiple configurations #3257

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
2 tasks done
voxmachinaio opened this issue Jun 29, 2022 · 5 comments
Open
2 tasks done

Comments

@voxmachinaio
Copy link

I am attempting to replace the default headerPattern and headerCorrespondence values of @commitlint/config-conventional by using a plugin module.

My plugin module looks like this:

index.js

module.exports = {
  parserPreset: {
    parserOpts: {
      headerPattern: /^(?:([A-Z]{2,}-\d+(?:, [A-Z]{2,}-\d+)*) \| ){1}(\w*)(?:\((\w*)\))?!?: (.+)$/,
      headerCorrespondence: [`jiraIds`, `type`, `scope`, `subject`],
    },
  },
  rules: {
    'header-max-length': [2, `always`, 110],
    'jira-empty': [2, `always`],
  },
  plugins: [
    {
      rules: {
        'jira-empty': ({ jiraIds }) => {
          const test = jiraIds ? true : false;
          return [
            test,
            `jira id(s) may not be empty`,
          ]
        },
      }
    }
  ]
}

package.json

{
  "name": "@acse/commitlint-config",
  "version": "0.4.0",
  "description": "ACSE Commitlint Config",
  "files": [
    "index.js"
  ],
  "keywords": [
    "acse",
    "commitlint"
  ],
  "peerDependencies": {
    "@commitlint/cli": "^17.0.3",
    "@commitlint/config-conventional": "^17.0.3"
  }
}

Expected Behavior

From a test git repository with @commitlint/cli, @commitlint/config-conventional, and @acse/commitlint-config (the plugin module) installed locally, run:

echo 'JIRA-111, JIRA-222 | feat(scope): subject' | ./node_modules/.bin/commitlint -x @commitlint/config-conventional @acse/commitlint-config

My expectation would be that this command exits in success (since that header line passes the headerPattern regex set in the plugin module's config.

Current Behavior

Instead that command exits in failure because the parserPreset in the resolved config has been overwritten by the parserPreset that @commitlint/config-conventional sets (note the addition of --print-config):

# echo 'JIRA-111, JIRA-2222 | feat(scope): subject' | ./node_modules/.bin/commitlint -x @commitlint/config-conventional @acse/commitlint-config --print-config

{
  extends: [ '@commitlint/config-conventional', '@acse/commitlint-config' ],
  formatter: '/home/voxmachina/dev/test/test/node_modules/@commitlint/format/lib/index.js',
  parserPreset: {
    parserOpts: {
      headerPattern: /^(\w*)(?:\((.*)\))?!?: (.*)$/,
      breakingHeaderPattern: /^(\w*)(?:\((.*)\))?!: (.*)$/,
      headerCorrespondence: [ 'type', 'scope', 'subject' ],
      noteKeywords: [ 'BREAKING CHANGE', 'BREAKING-CHANGE' ],
      revertPattern: /^(?:Revert|revert:)\s"?([\s\S]+?)"?\s*This reverts commit (\w*)\./i,
      revertCorrespondence: [ 'header', 'hash' ],
      issuePrefixes: [ '#' ]
    },
    name: 'conventional-changelog-conventionalcommits',
    path: './node_modules/conventional-changelog-conventionalcommits/index.js'
  },
  ignores: undefined,
  defaultIgnores: undefined,
  plugins: { local: { rules: { 'jira-empty': [Function: jira-empty] } } },
  rules: {
    'body-leading-blank': [ 1, 'always' ],
    'body-max-line-length': [ 2, 'always', 100 ],
    'footer-leading-blank': [ 1, 'always' ],
    'footer-max-line-length': [ 2, 'always', 100 ],
    'header-max-length': [ 2, 'always', 110 ],
    'subject-case': [
      2,
      'never',
      [ 'sentence-case', 'start-case', 'pascal-case', 'upper-case' ]
    ],
    'subject-empty': [ 2, 'never' ],
    'subject-full-stop': [ 2, 'never', '.' ],
    'type-case': [ 2, 'always', 'lower-case' ],
    'type-empty': [ 2, 'never' ],
    'type-enum': [
      2,
      'always',
      [
        'build',  'chore',
        'ci',     'docs',
        'feat',   'fix',
        'perf',   'refactor',
        'revert', 'style',
        'test'
      ]
    ],
    'jira-empty': [ 2, 'always' ]
  },
  helpUrl: 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint',
  prompt: {
    questions: {
      type: {
        description: "Select the type of change that you're committing",
        enum: {
          feat: {
            description: 'A new feature',
            title: 'Features',
            emoji: ''
          },
          fix: { description: 'A bug fix', title: 'Bug Fixes', emoji: '🐛' },
          docs: {
            description: 'Documentation only changes',
            title: 'Documentation',
            emoji: '📚'
          },
          style: {
            description: 'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',
            title: 'Styles',
            emoji: '💎'
          },
          refactor: {
            description: 'A code change that neither fixes a bug nor adds a feature',
            title: 'Code Refactoring',
            emoji: '📦'
          },
          perf: {
            description: 'A code change that improves performance',
            title: 'Performance Improvements',
            emoji: '🚀'
          },
          test: {
            description: 'Adding missing tests or correcting existing tests',
            title: 'Tests',
            emoji: '🚨'
          },
          build: {
            description: 'Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)',
            title: 'Builds',
            emoji: '🛠'
          },
          ci: {
            description: 'Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)',
            title: 'Continuous Integrations',
            emoji: '⚙️'
          },
          chore: {
            description: "Other changes that don't modify src or test files",
            title: 'Chores',
            emoji: '♻️'
          },
          revert: {
            description: 'Reverts a previous commit',
            title: 'Reverts',
            emoji: '🗑'
          }
        }
      },
      scope: {
        description: 'What is the scope of this change (e.g. component or file name)'
      },
      subject: {
        description: 'Write a short, imperative tense description of the change'
      },
      body: { description: 'Provide a longer description of the change' },
      isBreaking: { description: 'Are there any breaking changes?' },
      breakingBody: {
        description: 'A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself'
      },
      breaking: { description: 'Describe the breaking changes' },
      isIssueAffected: { description: 'Does this change affect any open issues?' },
      issuesBody: {
        description: 'If issues are closed, the commit requires a body. Please enter a longer description of the commit itself'
      },
      issues: {
        description: 'Add issue references (e.g. "fix #123", "re #123".)'
      }
    }
  }
}

You can see that it does correctly merge the local plugin definition and additional rule from the plugin module, but it overwrites the parserPreset value entirely.

If I don't additionally extend the configuration with with @commitlint/config-conventional I do get the correct headerPattern and headerCorrespondence values, but then I lose everything else extending @commitlint/config-conventional gives me (all the default rules, prompt definitions, etc):

# echo 'JIRA-111, JIRA-222 | feat(scope): subject' | ./node_modules/.bin/commitlint -x @acse/commitlint-config --print-config

{
  extends: [ '@acse/commitlint-config' ],
  formatter: '/home/voxmachina/dev/test/test/node_modules/@commitlint/format/lib/index.js',
  parserPreset: {
    parserOpts: {
      headerPattern: /^(?:([A-Z]{2,}-\d+(?:, [A-Z]{2,}-\d+)*) \| ){1}(\w*)(?:\((\w*)\))?!?: (.+)$/,
      headerCorrespondence: [ 'jiraIds', 'type', 'scope', 'subject' ]
    }
  },
  ignores: undefined,
  defaultIgnores: undefined,
  plugins: { local: { rules: { 'jira-empty': [Function: jira-empty] } } },
  rules: {
    'header-max-length': [ 2, 'always', 110 ],
    'jira-empty': [ 2, 'always' ]
  },
  helpUrl: 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint',
  prompt: {}
}

Affected packages

  • cli
  • config-conventional

Possible Solution

Not sure, but my expectation based on reading the documentation and gut instinct would be that the parserPreset options are merged into the ones provided by packages extended "before" my plugin package (when defining the extensions via the -x option on the commitlint cli, i.e.:

Steps to Reproduce (for bugs)

  1. Create plugin module as detailed above
  2. Create test git repository that installs @commitlint/cli, @commitlint/config-conventional, and <plugin_module> locally
  3. Run the above commands, validating that the parserPreset gets overwritten

Context

Essentially I am attempting to "extend" the default rules and configuration of the @commitlint/config-conventional module with headerPattern and headerCorrespondence values that should inform the parser to parse out a superset of values that are expected from the header (jiraIds, type, scope, subject).

Your Environment

Executable Version
commitlint --version 17.0.3
git --version 2.25.1
node --version 16.14.2
@voxmachinaio
Copy link
Author

Some things I've tried:

  1. Adding extends: ['@commitlint/config-conventional'] to the plugin module's exported configuration: the result is the same, parserPreset is overwritten
  2. Switching the order of "extension" in the commitlint cli (i.e. commitlint -x @acse/commitlint-config @commitlint/config-conventional): the result is the same, parserPreset is overwritten

@escapedcat
Copy link
Member

Thanks for the issue and the research!
I doubt we habe time to look into this but if you are motivated to dig into this we could try to support.

@voxmachinaio
Copy link
Author

I was able to find a solution! I'm not sure what the technical mechanism was that was producing the issue but the fix was to factor out the parserPreset object into a separate file, i.e.:

instead of:

module.exports = {
  parserPreset: {
    parserOpts: {
      headerPattern: /^(?:([A-Z]{2,}-\d+(?:, [A-Z]{2,}-\d+)*) \| ){1}(\w*)(?:\((\w*)\))?!?: (.+)$/,
      headerCorrespondence: [`jiraIds`, `type`, `scope`, `subject`],
    },
  },
  rules: {
    'header-max-length': [2, `always`, 110],
    'jira-empty': [2, `always`],
  },
  plugins: [
    {
      rules: {
        'jira-empty': ({ jiraIds }) => {
          const test = jiraIds ? true : false;
          return [
            test,
            `jira id(s) may not be empty`,
          ]
        },
      }
    }
  ]
};

do this:

module.exports = {
  parserPreset: './parser-preset',
  rules: {
    'header-max-length': [2, `always`, 110],
    'jira-empty': [2, `always`],
  },
  plugins: [
    {
      rules: {
        'jira-empty': ({ jiraIds }) => {
          const test = jiraIds ? true : false;
          return [
            test,
            `jira id(s) may not be empty`,
          ]
        },
      }
    }
  ]
};

and in parser-preset.js

module.exports = {
  parserOpts: {
    headerPattern: /^(?:([A-Z]{2,}-\d+(?:, [A-Z]{2,}-\d+)*) \| ){1}(\w*)(?:\((\w*)\))?!?: (.+)$/,
    headerCorrespondence: [`jiraIds`, `type`, `scope`, `subject`],
  },
};

Notes from Debugging:

  • Doing a bunch of monkeypatching in @commitlint/cli and @commitlint/load leads me to believe that the issue lies somewhere in the interaction of these two components, but I was unable to track down exactly what was happening before finding the fix

@escapedcat
Copy link
Member

escapedcat commented Jun 30, 2022

Awesome! Good to know, thanks! At least we have a workaround :)

@driskell
Copy link

driskell commented Oct 3, 2022

I am finding this impossible. With the comment above the parseOpts ends up ignored as it's not within a parserPreset.

It seems the @commitlint/resolve-extends is unable to merge parserOpts because when you are referencing @commitlint/config-conventional it's now a function that is returned for parserOpts, which of course can't merge so it just overwrites.

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

No branches or pull requests

2 participants