Skip to content

fix(load): resolve async config exports in CJS projects#4659

Merged
escapedcat merged 1 commit intoconventional-changelog:masterfrom
omar-y-abdi:fix/4557-cjs-async-config
Mar 15, 2026
Merged

fix(load): resolve async config exports in CJS projects#4659
escapedcat merged 1 commit intoconventional-changelog:masterfrom
omar-y-abdi:fix/4557-cjs-async-config

Conversation

@omar-y-abdi
Copy link
Copy Markdown
Contributor

@omar-y-abdi omar-y-abdi commented Mar 14, 2026

Problem

When a CJS config exports a Promise (module.exports = createPreset() where createPreset is async) or a function (module.exports = async () => config), @commitlint/load passes the raw Promise/function to validateConfig instead of the resolved config object.

Fix

In load.ts, resolve the loaded config before validation: if it's a function, call it; then await the result. Plain objects pass through unchanged.

This works because @commitlint/load already runs in an async context. The raw export from cosmiconfig was passed directly to validateConfig without resolving it first. By awaiting the value (and calling it if it's a function), the resolved config object reaches validation and the rest of the pipeline correctly.

Tests

  • Config exported as Promise.resolve({...}) resolves correctly
  • Config exported as async () => ({...}) resolves correctly
  • All 59 existing tests pass

Fixes #4557
Closes #4557

Config files that export a Promise or async/sync function are now
resolved before validation. This enables CJS projects to use patterns
like `module.exports = createPreset()` (returning a Promise) or
`module.exports = async () => config`.

Closes conventional-changelog#4557
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Resolve async config exports in CJS projects

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Resolve async config exports before validation in CJS projects
• Support Promise and async function config exports
• Add test fixtures for async config patterns
Diagram
flowchart LR
  A["Config loaded from file"] --> B{"Is function?"}
  B -->|Yes| C["Call function"]
  B -->|No| D["Await Promise"]
  C --> E["Await result"]
  D --> F["Resolved config"]
  E --> F
  F --> G["Validate config"]
Loading

Grey Divider

File Changes

1. @commitlint/load/src/load.ts 🐞 Bug fix +6/-2

Resolve async config before validation

• Check if loaded config is a function and call it before awaiting
• Await the config value to resolve Promises
• Pass resolved config to validation instead of raw export

@commitlint/load/src/load.ts


2. @commitlint/load/src/load.test.ts 🧪 Tests +22/-0

Add async config export tests

• Add test for config exported as Promise.resolve()
• Add test for config exported as async function
• Both tests verify correct rule resolution

@commitlint/load/src/load.test.ts


3. @commitlint/load/fixtures/async-config-promise/commitlint.config.cjs 🧪 Tests +7/-0

Promise config export test fixture

• New test fixture exporting config as Promise.resolve()
• Defines body-case rule with specific configuration

@commitlint/load/fixtures/async-config-promise/commitlint.config.cjs


View more (3)
4. @commitlint/load/fixtures/async-config-promise/package.json 🧪 Tests +3/-0

Promise fixture package metadata

• Package metadata for async-config-promise test fixture

@commitlint/load/fixtures/async-config-promise/package.json


5. @commitlint/load/fixtures/async-config-function/commitlint.config.cjs 🧪 Tests +7/-0

Async function config export test fixture

• New test fixture exporting config as async function
• Defines body-case rule with specific configuration

@commitlint/load/fixtures/async-config-function/commitlint.config.cjs


6. @commitlint/load/fixtures/async-config-function/package.json 🧪 Tests +3/-0

Async function fixture package metadata

• Package metadata for async-config-function test fixture

@commitlint/load/fixtures/async-config-function/package.json


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Mar 14, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Async extends not resolved 🐞 Bug ✓ Correctness
Description
load() now resolves only the top-level loaded.config, but extended/shareable configs loaded by
resolveExtends() are still spread into an object without calling/awaiting when the module export
is a Promise or function, which drops the extended config contents. This makes extends chains
still fail (or silently ignore config) for the same async-export patterns this PR adds support for
at the top level.
Code

@commitlint/load/src/load.ts[R46-51]

+		const resolvedConfig =
+			typeof loaded.config === "function"
+				? await loaded.config()
+				: await loaded.config;
+		validateConfig(loaded.filepath || "", resolvedConfig);
+		config = resolvedConfig;
Evidence
The PR adds resolution only for the cosmiconfig result (loaded.config) before validateConfig,
but the pipeline later loads extended configs via resolveExtends(). In
@commitlint/resolve-extends, imported configs are shallow-copied via object spread; if an extended
config exports a Promise or function, spreading it produces an empty object, effectively discarding
the extended configuration.

@commitlint/load/src/load.ts[45-52]
@commitlint/load/src/load.ts[78-83]
@commitlint/resolve-extends/src/index.ts[16-25]
@commitlint/resolve-extends/src/index.ts[132-147]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`@commitlint/load` now resolves async exports only for the top-level config loaded via cosmiconfig. Shareable/extended configs loaded through `resolveExtends()` can still export a Promise or function; `resolve-extends` currently shallow-copies the import with object spread, which collapses Promise/function exports into `{}` and drops the config.

### Issue Context
The PR fixed #4557 for the root config (`loaded.config`) by calling/awaiting it before `validateConfig`. The `extends` chain is loaded elsewhere (`@commitlint/resolve-extends`) and still assumes the imported export is a plain object.

### Fix Focus Areas
- @commitlint/load/src/load.ts[45-52]
- @commitlint/load/src/load.ts[78-83]
- @commitlint/resolve-extends/src/index.ts[16-25]
- @commitlint/resolve-extends/src/index.ts[132-147]
- @commitlint/resolve-extends/src/index.ts[139-171]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

2. Thenable config misawait 🐞 Bug ⛯ Reliability
Description
The new await loaded.config will treat any thenable object (an object with a then function) as a
Promise and invoke it, which can hang or execute unexpected code if a config object happens to
include such a property. Since the config schema does not disallow extra top-level keys, this is a
possible (though uncommon) edge case.
Code

@commitlint/load/src/load.ts[R46-49]

+		const resolvedConfig =
+			typeof loaded.config === "function"
+				? await loaded.config()
+				: await loaded.config;
Evidence
load() now unconditionally awaits non-function exports. The config schema defines known properties
but does not set additionalProperties: false at the top level, so extra keys are permitted; an
exported object with a then function would be treated as a thenable by await.

@commitlint/load/src/load.ts[45-51]
@commitlint/config-validator/src/commitlint.schema.json[1-4]
@commitlint/config-validator/src/commitlint.schema.json[96-105]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`await loaded.config` will assimilate any thenable object (object with a `then` function). While rare, commitlint configs allow extra top-level keys, so this could cause unexpected hangs/behavior.

### Issue Context
The PR introduced unconditional `await` for non-function exports to support `module.exports = Promise.resolve(config)`.

### Fix Focus Areas
- @commitlint/load/src/load.ts[45-51]
- @commitlint/config-validator/src/commitlint.schema.json[1-4]
- @commitlint/config-validator/src/commitlint.schema.json[96-105]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@codesandbox-ci
Copy link
Copy Markdown

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Comment thread @commitlint/load/src/load.ts
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes @commitlint/load so that when a CommonJS config exports a Promise or a (async) function, the loader resolves it to a plain config object before calling validateConfig, ensuring validation and the rest of the load pipeline work correctly.

Changes:

  • Resolve loaded config exports by calling function exports and awaiting Promise/function results before validation.
  • Add test coverage for CJS configs exporting a Promise and an async function.
  • Add new fixtures covering both async export patterns.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
@commitlint/load/src/load.ts Resolves loaded.config (call if function, then await) before validation and merge.
@commitlint/load/src/load.test.ts Adds tests asserting async config exports are resolved into rules correctly.
@commitlint/load/fixtures/async-config-promise/package.json New fixture package metadata for Promise-export config.
@commitlint/load/fixtures/async-config-promise/commitlint.config.cjs New fixture exporting Promise.resolve({...}).
@commitlint/load/fixtures/async-config-function/package.json New fixture package metadata for function-export config.
@commitlint/load/fixtures/async-config-function/commitlint.config.cjs New fixture exporting async () => ({...}).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

@escapedcat escapedcat merged commit fce263f into conventional-changelog:master Mar 15, 2026
16 checks passed
This was referenced Mar 15, 2026
This was referenced Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

fix: emoji with CommumJS

3 participants