question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

parserPreset is overwritten when extending multiple configurations

See original GitHub issue

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

Issue Analytics

  • State:open
  • Created a year ago
  • Comments:5

github_iconTop GitHub Comments

1reaction
driskellcommented, 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.

1reaction
voxmachinaiocommented, Jun 30, 2022

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
Read more comments on GitHub >

github_iconTop Results From Across the Web

When reading multiple config files, ConfigParser overwrites ...
I have two different configuration files, each with a Section 1 . The two files have differing numbers of options, but the one...
Read more >
Diff - platform/tools/tradefederation - Google Git
Semaphore; + +/** + * A harness that test video playback with multiple devices and reports result. + */ +public class VideoMultimeterRunner extends...
Read more >
Plaso (log2timeline) - Read the Docs
If you want to overwrite the automatic selection of parsers you can define them using the --parsers parameter. ... Plaso comes with multiple...
Read more >
Channel in a Box Lite User Manual - HubSpot
You can resize it by stretching its edges. The image to the right shows the Volume control window of an AirBox that has...
Read more >
Index (univocity-html-parser 2.1.0 API)
Creates a processor for java beans of multiple types ... All parsers must support, at the very least, the settings provided by CommonParserSettings...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found