Skip to content

Support callbacks in config JS/TS/ files to avoid the need for require() in config files#863

Open
jdmarshall wants to merge 7 commits intonode-config:masterfrom
jdmarshall:deferredESM
Open

Support callbacks in config JS/TS/ files to avoid the need for require() in config files#863
jdmarshall wants to merge 7 commits intonode-config:masterfrom
jdmarshall:deferredESM

Conversation

@jdmarshall
Copy link
Collaborator

@jdmarshall jdmarshall commented Jan 15, 2026

This fixes #740 and provides a solution for a number of issues preventing ESM migration.

Summary by CodeRabbit

  • New Features

    • Deferred configuration can now resolve async values (promises) during initialization.
    • Config modules can be provided as factory functions receiving dependencies for dynamic config generation.
  • Documentation

    • Deprecation notices added for legacy async/defer patterns and guidance toward the new approach.
  • Chores

    • Copyright years updated to 2026.
  • Tests

    • Expanded tests for async/deferred config resolution; some legacy deferred tests were removed.

✏️ Tip: You can customize this high-level summary in your review settings.

…fig()

This should answer node-config#740 and also opens the door for migrating to ESM modules.

This does not at present address the issue of async.
And move some integration tests to unit tests.

Prep work for adding async support to defer()
@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

Replaces in-file defer implementation with a new lib/defer module and re-exports, adds Util.resolveAsyncConfigs() and support for invoking config files as factory functions with injected {defer, util}, updates docs/deprecation notes, tests, and copyright years.

Changes

Cohort / File(s) Summary
License & Docs
LICENSE, README.md
Updated copyright year from 2010-2025 to 2010-2026.
Async API docs
async.js
Added @deprecated JSDoc notes for asyncConfig and resolveAsyncConfigs; no runtime changes.
Defer implementation move
defer.js, lib/defer.js
Removed old in-file DeferredConfig implementation in defer.js, now re-exports deferConfig and DeferredConfig from ./lib/defer; new lib/defer.js implements deferred resolution (sync+async) and exposes deferConfig/DeferredConfig.
Core util changes
lib/util.js
Added static async resolveAsyncConfigs(config) to collect and resolve Promise leaves; changed config file loading to call exported config functions with ({defer, util}); updated deferred-resolution traversal.
Tests — added/modified
test/0-util.js, test/3-config/default.js, test/3-deferred-configs.js
test/0-util.js: added many tests for deferred/async resolution and interactions; test/3-config/default.js: switched from static export to factory module.exports = ({defer, util}) => ({...}); test/3-deferred-configs.js: removed several older defer behavior tests.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Loader as Config Loader
    participant ConfigFn as Config Function
    participant Util as Util
    participant Defer as lib/defer

    App->>Loader: require/load config file
    Loader->>ConfigFn: invoke with ({defer: Defer.deferConfig, util: Util})
    ConfigFn->>Defer: create deferred values (deferConfig)
    ConfigFn-->>Loader: return config object (may contain DeferredConfig or Promises)
    Loader->>Util: Util.resolveAsyncConfigs(config)
    Util->>Util: traverse config, collect Promises/Deferreds
    Util->>Defer: prepare/resolve deferreds (resolve may return Promises)
    Util->>Util: Promise.all on collected promises
    Util-->>App: finalized, resolved config object
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

breaking-change

Suggested reviewers

  • markstos

Poem

🐰 I nibbled code where deferred seeds lay,
I planted getters that wake and say "play",
Promises bloom when util tends the plot,
A config garden—resolved on the dot. 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding support for callbacks in config files to avoid require() calls, which directly addresses the linked issue about esbuild bundling.
Linked Issues check ✅ Passed The PR fulfills issue #740 by making defer accessible from package main through lib/defer.js refactoring and re-exports, enabling bundlers like esbuild to locate and include it.
Out of Scope Changes check ✅ Passed All changes are scoped to the linked issue: copyright updates are maintenance, deprecation notices clarify the refactoring, and the new callback/async mechanism supports the primary objective of enabling defer usage with bundlers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
lib/defer.js (1)

14-22: Consider documenting the double-assignment pattern for async resolution.

The pattern of setting the property first to the promise (line 18) then to the resolved value (line 19) is clever—it allows intermediate access to get the in-flight promise. However, this behavior isn't immediately obvious.

A brief inline comment explaining this design choice would help future maintainers understand the intent.

📝 Suggested documentation
     if (isAsyncFunction(func)) {
       obj.resolve = async function () {
         const promise = func.call(config, config, original);
 
+        // First, expose the promise for intermediate access, then replace with resolved value
         Object.defineProperty(prop, property, { value: promise });
         Object.defineProperty(prop, property, { value: await promise });
 
         return promise;
       }
defer.js (2)

3-6: Clarify the deprecation message with actionable guidance.

The current deprecation message mentions "the new callback mechanism" without explaining what it is or how users should migrate. Consider either:

  1. Specifying the new import path if users should import directly from lib/defer
  2. Explaining what "callback mechanism" refers to (e.g., passing factory functions to config files)
📝 Suggested improvement
 /**
- * `@deprecated` please use the new callback mechanism
- * `@see` lib/defer.js
+ * `@deprecated` Import from 'config/lib/defer' instead, or use the config file callback pattern
+ * which provides defer injection. See lib/defer.js for implementation details.
  */

9-12: Add @see reference for consistency with deferConfig deprecation.

The DeferredConfig deprecation is missing the @see reference that's present in the deferConfig deprecation above.

📝 Suggested fix
 /**
  * `@deprecated` please use the new callback mechanism
+ * `@see` lib/defer.js
  */
 module.exports.DeferredConfig = DeferredConfig;

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fa6eab and e6478c4.

📒 Files selected for processing (2)
  • defer.js
  • lib/defer.js
🧰 Additional context used
🧬 Code graph analysis (1)
lib/defer.js (1)
test/0-util.js (1)
  • deferConfig (15-15)
🔇 Additional comments (3)
lib/defer.js (3)

33-33: LGTM!

The lazy getter pattern correctly triggers resolution on first property access, and the resolve() method properly replaces the getter with the computed value, preventing redundant resolution calls.


41-42: LGTM!

Exports are correctly structured and maintain backward compatibility with existing consumers like test/0-util.js.


1-6: LGTM!

The DeferredConfig constructor with prototype methods for prepare and resolve provides a clean way to identify deferred configurations via instanceof checks. The use of node:util/types for isAsyncFunction is appropriate and fully compatible with the project's minimum Node.js version 20.0.0+.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jdmarshall jdmarshall marked this pull request as draft January 15, 2026 04:44
@jdmarshall jdmarshall marked this pull request as ready for review January 15, 2026 04:53
This will probably be better overall for anyone doing an incremental
migration since the new code will run last.
@jdmarshall
Copy link
Collaborator Author

jdmarshall commented Jan 19, 2026

This feature is critical to breaking the chicken and egg problems with the config singleton and moving this module to ESM. I have a PR almost ready to go for ESM migration that requires this PR to land. I’d appreciate a timely review while I still have momentum.

Because it deprecates several features, I'd like this to be in 4.3 so that people have plenty of time to adapt before I remove those deprecations in 5.0, which will absolutely be necessary in order to get import to work. You cannot import from modules that are currently being imported, and that is exactly what async.js and defer.js are doing.

Using the callback mechanism means we are exposing the parts of node-config that are stateless or have private state, and thus have already finished importing by the time deferment is resolved.

@jdmarshall jdmarshall added this to the 4.3 milestone Jan 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot use defer with esbuild (fix: export defer from config)

1 participant