Thanks to visit codestin.com
Credit goes to github.com

Skip to content

ESM conversion of node-config#889

Merged
jdmarshall merged 6 commits intonode-config:masterfrom
jdmarshall:imports
Mar 1, 2026
Merged

ESM conversion of node-config#889
jdmarshall merged 6 commits intonode-config:masterfrom
jdmarshall:imports

Conversation

@jdmarshall
Copy link
Collaborator

@jdmarshall jdmarshall commented Feb 19, 2026

Summary by CodeRabbit

  • Breaking Changes

    • Removed deprecated async/deferred config utilities and raw() helper; minimum Node.js engine now >= 20.11.0.
  • New Features

    • Full ES Module support across the project.
    • New runtime configuration module with immutable-by-default accessors, get/has/getEnv, serialization, file-based loading, module-defaults and immutability helpers.
  • Chores

    • Modernized configuration initialization and startup flow.
    • Tests and TypeScript typings updated for ESM and new config API.

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Removed deprecated async/raw helpers and CommonJS wrappers; migrated core modules and most tests to ES modules; added a new ES-module Config implementation at lib/config.mjs (ConfigClass/ConfigUtils) and made lib/config.js delegate to it; updated package.json module field and Node engine.

Changes

Cohort / File(s) Summary
Removed deprecated helpers
async.js, raw.js
Deleted async.js and raw.js and their public exports (asyncConfig, resolveAsyncConfigs, raw, RawConfig).
Defer API surface
defer.js, lib/defer.js
Removed deprecated CommonJS wrapper exports from defer.js; lib/defer.js converted to ESM and now exports deferConfig/DeferredConfig via ES module syntax.
New config implementation
lib/config.mjs, lib/config.js
Added lib/config.mjs implementing ConfigClass and ConfigUtils (singleton default export); lib/config.js now re-exports config.mjs default.
Module migrations & parser
lib/util.js, parser.js
Converted modules to ESM, adjusted dynamic loading (createRequire/moduleRequire), set defaultParser use, and default-exported Parser.
Tests → ESM & helpers
test/..., test/_utils/requireUncached.*
Most tests migrated to ESM, switched to async hooks and import.meta.dirname; removed CJS test/_utils/requireUncached.js and added requireUncached.mjs (dynamic import cache-bust).
Removed async/defer tests & fixtures
test/15-*, test/type-files/async.ts, test/type-files/raw.ts, related configs
Deleted tests and fixtures exercising asyncConfig/resolveAsyncConfigs and raw APIs.
Test config shape changes
test/3-config/local.js, test/9-config/default.js, test/x-config-ts/*
Several test config modules changed from static exports to factory functions (accepting {defer} or {raw}) or restructured to load-time factories.
Package manifest
package.json
Added "module": "./lib/config.mjs", bumped engines.node to >= 20.11.0, and removed typesVersions entries for removed files.
Type/test adjustments
test/type-files/*, test/x-*
Adjusted type-test import paths and removed tests covering deleted APIs; updated imports to reflect default ESM exports.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant Load as Load.fromEnvironment
    participant Config as ConfigClass
    participant Env as Environment

    App->>Load: Load.fromEnvironment()
    Load->>Config: new ConfigClass(load)
    Config->>Env: read NODE_CONFIG / CLI overrides
    Config->>Config: _init(load) — apply overrides & attach proto
    Config->>Config: runStrictnessChecks() / makeImmutable*
    Config-->>App: export singleton instance
    App->>Config: get(property) / has(property)
    Config-->>App: return value
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

breaking-change

Suggested reviewers

  • markstos
  • lorenwest

Poem

🐰
I hopped through modules, swapped CJS for light ESM,
Burrowed out old async, hid raw under a stem.
ConfigClass sprouts with defaults snug and tight,
Tests wake with import.meta and async night,
I chew a carrot, approve the new sight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'ESM conversion of node-config' clearly and concisely summarizes the primary objective of converting the codebase from CommonJS to ES modules, which aligns with the extensive file-level changes shown in the raw summary.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 added this to the 5.0 milestone Feb 19, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
lib/util.js (2)

1391-1398: ⚠️ Potential issue | 🔴 Critical

Bug: require is undefined in ESM — hostname resolution silently fails.

require('os') on line 1393 will throw a ReferenceError because this file is now an ES module and has no require in scope. The try/catch silently swallows the error and falls through to hostName = '', so the actual OS hostname is never used when HOST/HOSTNAME env vars are absent.

OS is already imported at the top of the file (line 10: import OS from 'node:os'). Remove the redundant require and use the existing import directly.

Proposed fix
     try {
       if (!hostName) {
-        const OS = require('os');
         hostName = OS.hostname();
       }
     } catch (e) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/util.js` around lines 1391 - 1398, The hostname resolution try/catch
currently calls require('os') (which fails under ESM) and gets swallowed;
replace the dynamic require with the already-imported OS module and call
OS.hostname() to set hostName when hostName is falsy, keeping the existing catch
behavior (i.e., set hostName = '' on error). Update the block that references
require('os') so it uses the imported OS symbol and OS.hostname() instead of
require.

456-491: ⚠️ Potential issue | 🔴 Critical

resolveAsyncConfigs does not write resolved values back to the config object.

The method collects Promise instances and awaits them via Promise.all(), but the resolved values are never assigned back. Unlike resolveDeferredConfigs which uses a prepare/resolve pattern to track locations and update values in place, resolveAsyncConfigs has no mechanism to write values back to their original config paths. Tests confirm this is a bug—assertions expect resolved values to be present in the config object after this method completes. Since this method replaces the deprecated ./async.js, callers expect the same behavior as before, including access to resolved values via config.get().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/util.js` around lines 456 - 491, resolveAsyncConfigs currently gathers
Promise instances in promises[] but never writes their resolved values back into
the original config structure; update it to track each promise's target location
and assign the resolved value back into the config (for object properties and
array entries) after awaiting Promise.all. Modify the traversal in _iterate to,
when encountering a Promise, push an object containing the promise and a setter
reference (e.g., { promise, parent, keyOrIndex }) or push the promise alongside
its target index in a separate array; after awaiting Promise.all, iterate
results and use the stored parent/keyOrIndex to set the resolved values into the
original config so config.get() returns resolved values. Ensure the logic
handles promises inside arrays and nested objects and preserves the existing
sort/traversal behavior in resolveAsyncConfigs.
test/0-util.js (2)

1637-1645: ⚠️ Potential issue | 🟠 Major

Test 'loads the values in left-right order' accepts a done callback but never calls it — will time out in node:test.

In node:test, a test function that declares a done parameter is treated as callback-style and the framework waits for done() to be called. Since done is never invoked here (the body is synchronous), this test will hang until it times out.

🐛 Proposed fix
-    it('loads the values in left-right order', function(done) {
+    it('loads the values in left-right order', function() {
       const config = util.loadFileConfigs({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/0-util.js` around lines 1637 - 1645, The test "loads the values in
left-right order" declares a callback parameter `done` but runs synchronously
and never calls it, causing node:test to hang; fix by removing the `done`
parameter from the test function signature (so it runs as a synchronous test) or
alternatively call `done()` after the assertions; update the test that invokes
util.loadFileConfigs and the surrounding it(...) callback (the function
containing the assertion for config.db.name) to use the synchronous form (no
`done`) or to call `done()` explicitly.

761-820: ⚠️ Potential issue | 🟠 Major

Update engines field in package.json to >= 20.11.0import.meta.dirname is unavailable in Node.js < 20.11.0.

The import.meta.dirname property is used extensively throughout the test files and requires Node.js v20.11.0 or later. The current engine constraint >= 20.0.0 allows Node.js versions 20.0.0–20.10.x where this property is undefined, causing silent path resolution failures. Update the engines field to enforce the minimum version requirement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/0-util.js` around lines 761 - 820, Tests use import.meta.dirname (seen
in calls to load.loadFile with Path.join(import.meta.dirname, ...)), which is
only available in Node >=20.11.0; update the package.json "engines" field from
">= 20.0.0" to ">= 20.11.0" so CI and consumers run a compatible runtime, and
rerun tests to confirm path resolution issues are resolved.
test/1-protected-test.js (1)

27-45: ⚠️ Potential issue | 🟠 Major

process.argv.push() in beforeEach accumulates entries across tests — use assignment instead.

process.argv.push(...) runs before each test, so after 11 tests in this suite the argv array contains 11 copies of --NODE_CONFIG=.... Each call to requireUncached('./lib/config.mjs') will see a progressively longer argv, potentially causing undefined or inconsistent config behavior for later tests.

Compare with the correct pattern already used in test/2-config-test.js line 30:

process.argv = [undefined, undefined, '--NODE_CONFIG={"EnvOverride":...}'];
🐛 Proposed fix
-    process.argv.push('--NODE_CONFIG={"EnvOverride":{"parm5":"overridden from --NODE_CONFIG","parm6":101}}');
+    process.argv = [
+      process.argv[0],
+      process.argv[1],
+      '--NODE_CONFIG={"EnvOverride":{"parm5":"overridden from --NODE_CONFIG","parm6":101}}'
+    ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/1-protected-test.js` around lines 27 - 45, The test's beforeEach uses
process.argv.push(...) which accumulates the NODE_CONFIG flag across tests;
replace the push with an assignment that sets process.argv to a clean array
containing the NODE_CONFIG entry (matching the pattern used in 2-config-test.js)
so each call to requireUncached('./lib/config.mjs') sees a consistent argv;
update the beforeEach block (the process.argv handling) to assign a fresh array
(e.g., with two leading entries and the '--NODE_CONFIG=...' string) instead of
pushing.
test/6-strict-mode.js (1)

74-97: ⚠️ Potential issue | 🟡 Minor

error is never reset at the start of beforeEach, leaving stale state risk across the two it blocks.

Each closure from _expectException contains two it blocks, meaning beforeEach runs twice. If the first run sets error and the second run completes without throwing (e.g., transient behavior in dynamic import caching), the second it will evaluate stale state and produce a misleading result.

🛠️ Proposed fix – reset `error` at the top of `beforeEach`
  return () => {
    beforeEach(async function() {
+     error = undefined;
      // Change the configuration directory for testing
      process.env.NODE_CONFIG_DIR         = import.meta.dirname + '/6-config';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/6-strict-mode.js` around lines 74 - 97, The shared variable error is not
reset at the start of the beforeEach closure, which can leave stale state across
the two it blocks; inside the beforeEach function (the closure returned by
_expectException), set error = undefined (or null) as the first statement before
modifying env and calling requireUncached('./lib/config.mjs') so each test run
starts with a clean error state.
🧹 Nitpick comments (14)
test/_utils/requireUncached.mjs (1)

9-9: process.cwd()-anchored resolution is fragile if tests are not run from the project root.

Path.resolve(process.cwd(), moduleName) works only when cwd equals the project root. Consider documenting this assumption or using import.meta of the calling test file instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/_utils/requireUncached.mjs` at line 9, The current resolution uses
process.cwd() which breaks if tests run outside the repo root; change the
resolver in requireUncached.mjs to resolve moduleName relative to the caller's
module URL instead of process.cwd(). Specifically, replace
Path.resolve(process.cwd(), moduleName) with a resolution based on
import.meta.url (convert to a file path with URL.fileURLToPath and use
Path.dirname(...)) so that requireUncached resolves modules relative to the test
file that imported it; alternatively, document the cwd assumption if you prefer
not to change behavior.
parser.js (2)

90-103: Defensive TS lazy-init looks correct, but require?. optional chaining is unnecessary.

require is defined on line 7 via createRequire(process.cwd()) and is always truthy. The ?. on line 91 can never short-circuit. Not harmful, but potentially confusing to readers about when require could be undefined.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@parser.js` around lines 90 - 103, In Parser.tsParser replace the unnecessary
optional chaining on require by changing require?.extensions['.ts'] to
require.extensions['.ts'] so the defensive TS lazy-init logic is clearer; locate
the check inside the Parser.tsParser function and update that conditional
expression only (leave the rest of the TS lazy-init and TS.register call
intact).

6-7: moduleRequire uses an inefficient module search path — clarify or refactor.

createRequire(Path.join(process.cwd(), 'node_modules')) causes module resolution to search /node_modules/node_modules first, then fall back to /node_modules. This is less efficient than createRequire(process.cwd()) which searches /node_modules directly. While both functions eventually find modules through parent directory fallback, moduleRequire wastes a search iteration on a non-existent path.

The intent appears to be distinguishing module loading (bare names like 'yaml') from file-based loading (by filename), but the implementation creates unnecessary inefficiency. Either pass a file path within node_modules (e.g., Path.join(process.cwd(), 'node_modules', 'package.json')) or reconsider whether this distinction is needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@parser.js` around lines 6 - 7, The createRequire call creating moduleRequire
is using Path.join(process.cwd(), 'node_modules') which forces an extra,
unnecessary lookup; change the moduleRequire createRequire argument to either
createRequire(process.cwd()) to search node_modules directly or provide a
concrete file inside node_modules (e.g., Path.join(process.cwd(),
'node_modules', 'package.json')) so resolution does not probe a non-existent
nested node_modules—update the moduleRequire declaration accordingly (symbols:
moduleRequire, require, createRequire, Path, process.cwd()).
test/16-custom-parser.js (1)

78-80: after hook doesn't need to be async.

The body only performs a synchronous delete — the async qualifier is unnecessary here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/16-custom-parser.js` around lines 78 - 80, The after hook in
test/16-custom-parser.js is declared async but only performs a synchronous
delete of process.env.NODE_CONFIG_PARSER; remove the unnecessary async qualifier
from the anonymous function passed to after(...) so the hook is a plain
synchronous function.
package.json (1)

5-5: Consider using "exports" field with conditions for modern Node.js ESM support.

The "module" field is a de facto bundler convention (not an official Node.js field) and will be ignored by Node.js runtime. For official ESM resolution in Node.js, use the "exports" field with conditions like "import" and "require" to provide separate entry points. Currently, without an "exports" field, Node.js will resolve to the "main" entry (./lib/config.js) for all consumers; only explicit imports of the .mjs file will bypass it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 5, Add an "exports" field to package.json to provide
conditional entry points for ESM and CJS consumers instead of relying on the
non-standard "module" field; map the "import" condition to "./lib/config.mjs"
(ESM) and the "require" condition to "./lib/config.js" (CJS), and keep "main" as
fallback if desired, so Node's resolver uses the correct file for each module
system while preserving the existing "./lib/config.mjs" and "./lib/config.js"
artifacts referenced by "module" and "main".
lib/config.mjs (1)

598-615: filename.match(env) interprets env as a regex pattern.

Lines 604–605 use String.match() with the raw env value, which is treated as a regex. If NODE_ENV contains regex metacharacters (e.g., c++, test.v2), this can produce false matches or throw. Consider using filename.includes(env) or escaping the value. This is pre-existing behavior, so flagging as optional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/config.mjs` around lines 598 - 615, The code uses filename.match(env)
inside the load.options.nodeEnv loop which treats env as a regex and can
misbehave for values with regex metacharacters; change the check to perform a
literal substring or exact comparison instead — e.g., use filename.includes(env)
or escape env before using RegExp — so replace the filename.match(env) call in
the sourceFilenames.some callback (referencing sourceFilenames,
load.options.nodeEnv, filename.match and load.getEnv) with a safe literal
comparison and keep the existing _warnOrThrow calls unchanged.
test/9-raw-configs.js (1)

8-13: Minor formatting: closing brace on same line as assignment.

Line 13 has }); appended directly after the assignment statement. Likely an accidental collapse.

Proposed fix
-    CONFIG = await requireUncached('./lib/config.mjs');  });
+    CONFIG = await requireUncached('./lib/config.mjs');
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/9-raw-configs.js` around lines 8 - 13, The closing brace and parenthesis
for the before(async() => { ... }) hook are placed on the same line as the
CONFIG assignment, making the formatting look collapsed; update the before hook
around CONFIG = await requireUncached('./lib/config.mjs') so the final `});` is
on its own line (i.e., close the async function block and the before call on a
separate line) to restore proper readability and consistent formatting in
test/9-raw-configs.js.
test/11-regexp.js (1)

8-14: NODE_APP_INSTANCE='regexp' is set in beforeEach but never deleted — leaks into subsequent test files.

After this suite runs, NODE_APP_INSTANCE remains 'regexp', which could affect any subsequent test that calls requireUncached('./lib/config.mjs') without explicitly overriding it.

🔧 Add cleanup
-import { describe, it, beforeEach } from 'node:test';
+import { describe, it, beforeEach, afterEach } from 'node:test';
 ...
+  afterEach(function() {
+    delete process.env.NODE_APP_INSTANCE;
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/11-regexp.js` around lines 8 - 14, The test sets
NODE_APP_INSTANCE='regexp' in beforeEach which is not cleaned up; add an
afterEach (or restore logic) to remove or restore process.env.NODE_APP_INSTANCE
after each test that calls requireUncached('./lib/config.mjs') so the
environment doesn't leak into other suites; implement this cleanup in the same
test file (pair with the existing beforeEach) to either delete
process.env.NODE_APP_INSTANCE or reset it to its prior value.
test/8-config-extending.js (1)

27-36: NODE_CONFIG_DIR is mutated inside the it() body (line 28) after already being set in before() (line 13) — anti-pattern and redundant.

Setting process.env inside a test body bypasses the setup/teardown lifecycle and makes the test's environment invisible to readers scanning before/beforeEach hooks. Since line 13 already sets the same value, line 28 is also redundant.

♻️ Suggested fix — remove the redundant assignment
   it('Extending a configuration with another configuration should work without error', function () {
-    process.env.NODE_CONFIG_DIR = import.meta.dirname + '/8-config';
     let filename = path.join(process.env.NODE_CONFIG_DIR,'base-config.json');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/8-config-extending.js` around lines 27 - 36, The test mutates
process.env.NODE_CONFIG_DIR inside the it() block which is redundant because the
same value is set in the before() hook; remove the assignment to
process.env.NODE_CONFIG_DIR from the it('Extending a configuration...') test
body (the line that sets process.env.NODE_CONFIG_DIR = import.meta.dirname +
'/8-config') so the test relies on the before() setup, leaving the rest of the
test (reading base-config.json, calling CONFIG.util.attachProtoDeep, and
invoking Util.extendDeep) unchanged.
test/0-util.js (1)

1534-1544: Commented-out block references require.extensions, which is unavailable in ESM — remove it.

require.extensions is a CJS-only API. require.extensions is not used by import. The entire commented-out test can be removed; there is no ESM equivalent for checking a custom .ts extension handler.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/0-util.js` around lines 1534 - 1544, Remove the entire commented-out
test that references the CJS-only API require.extensions (the block that checks
existingHandler and asserts require.extensions['.ts'] after calling
util.loadFileConfigs with import.meta.dirname and '.ts'); simply delete those
commented lines since require.extensions is unavailable in ESM and there is no
ESM equivalent, leaving no replacement code.
test/21-date.js (1)

1-13: LGTM — before is appropriate here; consider path.join for cross-platform safety.

The one-time before hook is correct since all inner tests are read-only and do not mutate CONFIG. The only nit is the path string concatenation:

🔧 Optional: use `path.join` for cross-platform path construction
+import path from 'node:path';
 import { describe, it, before } from 'node:test';
 import assert from 'assert';
 import { requireUncached } from './_utils/requireUncached.mjs';

 describe('Tests for date', function() {
   let CONFIG;

   before(async function() {
-    process.env.NODE_CONFIG_DIR = import.meta.dirname + '/config';
+    process.env.NODE_CONFIG_DIR = path.join(import.meta.dirname, 'config');
     process.env.NODE_ENV = 'test';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/21-date.js` around lines 1 - 13, Replace the string concatenation for
NODE_CONFIG_DIR with a cross-platform path join: instead of using
import.meta.dirname + '/config' compute the directory from import.meta.url (via
url.fileURLToPath and path.dirname) and then use path.join(dir, 'config');
update the top of the test to import/require the Node 'path' and 'url' helpers
and keep the existing before hook, CONFIG variable, and
requireUncached('./lib/config.mjs') unchanged.
test/2-config-test.js (1)

62-147: Inner describe blocks load config via requireUncached without re-setting env vars — implicit dependency on Library initialization running first.

The Immutability, Configurations from $NODE_CONFIG, Configurations from --NODE_CONFIG, and other inner describe blocks each have a beforeEach that calls only requireUncached('./lib/config.mjs') with no env setup. They silently depend on NODE_CONFIG_DIR, NODE_ENV, NODE_APP_INSTANCE, NODE_CONFIG, and process.argv remaining set from the Library initialization beforeEach above — which only runs for tests within that specific inner describe.

This works today because node:test runs tests sequentially, but any of these inner suites run in isolation (or if test order changes) would load config from an indeterminate environment.

The shared setup belongs at the outer describe('Test suite for node-config') scope:

♻️ Suggested refactor (sketch)
 describe('Test suite for node-config', function() {
+  beforeEach(async function () {
+    process.env.NODE_CONFIG_DIR = import.meta.dirname + '/config';
+    process.env.NODE_ENV = 'test';
+    process.env.NODE_APP_INSTANCE = '3';
+    process.env.NODE_CONFIG = '{"EnvOverride":{"parm3":"overridden from $NODE_CONFIG","parm4":100}}';
+    process.argv = [undefined, undefined, '--NODE_CONFIG={"EnvOverride":{"parm5":"overridden from --NODE_CONFIG","parm6":101}}'];
+    process.env.CUSTOM_JSON_ENVIRONMENT_VAR = 'CUSTOM VALUE FROM JSON ENV MAPPING';
+    // ...remaining env setup...
+  });
+
   describe('Library initialization', function() {
     let config;
-    beforeEach(async function () {
-      process.env.NODE_CONFIG_DIR = import.meta.dirname + '/config';
-      // ...etc...
-      config = await requireUncached('./lib/config.mjs');
-    });
+    beforeEach(async function () { config = await requireUncached('./lib/config.mjs'); });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/2-config-test.js` around lines 62 - 147, The inner suites (Immutability,
Configurations from $NODE_CONFIG, Configurations from --NODE_CONFIG) call
requireUncached('./lib/config.mjs') without re-establishing environment,
implicitly depending on the outer "Test suite for node-config" setup; fix by
moving the shared env/argv initialization (NODE_CONFIG_DIR, NODE_ENV,
NODE_APP_INSTANCE, NODE_CONFIG, process.argv) into the outer describe's setup
(use before or beforeEach at the outer describe level) so every inner
requireUncached loads a deterministically configured environment, and add
matching cleanup (after or afterEach) to restore original env/argv; keep inner
beforeEach only calling requireUncached('./lib/config.mjs') so tests remain
isolated.
test/5-getConfigSources.js (1)

10-26: LGTM — but process.argv is left in a modified state after all tests in this file.

process.argv is replaced but never restored, leaving it as ["node","path/to/some.js","--NODE_CONFIG={}"] after the first describe's last test. Subsequent test files that rely on the original process.argv may be affected. Consider restoring with an after hook:

🔧 Suggested cleanup
+import { describe, it, beforeEach, after } from 'node:test';
 ...
 describe('tests with NODE_CONFIG env set...', function() {
+  let savedArgv;
+  after(() => { process.argv = savedArgv; });
   beforeEach(async function () {
+    savedArgv = savedArgv ?? process.argv;
     process.argv = ["node","path/to/some.js","--NODE_CONFIG={}"];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/5-getConfigSources.js` around lines 10 - 26, Save the original
process.argv at the start of the test setup and restore it in a teardown hook so
other tests aren't affected: in the beforeEach (where process.argv is set to
["node","path/to/some.js","--NODE_CONFIG={}"]) capture the original value (e.g.
const originalArgv = process.argv.slice()) and add an after or afterEach hook
that resets process.argv = originalArgv; ensure this lives alongside the
existing requireUncached('./lib/config.mjs') / sources =
config.util.getConfigSources() setup so tests clean up after themselves.
test/6-strict-mode.js (1)

77-97: No afterEach cleanup for process environment variables.

NODE_CONFIG_DIR, NODE_CONFIG_STRICT_MODE, NODE_APP_INSTANCE, NODE_ENV, and NODE_CONFIG_ENV are mutated in beforeEach but never restored after each test. If any other test suite runs in the same process after this one, it may inherit contaminated environment state.

♻️ Suggested `afterEach` teardown
  return () => {
+   let origEnv;
    beforeEach(async function() {
+     error = undefined;
+     origEnv = {
+       NODE_CONFIG_DIR:         process.env.NODE_CONFIG_DIR,
+       NODE_CONFIG_STRICT_MODE: process.env.NODE_CONFIG_STRICT_MODE,
+       NODE_APP_INSTANCE:       process.env.NODE_APP_INSTANCE,
+       NODE_ENV:                process.env.NODE_ENV,
+       NODE_CONFIG_ENV:         process.env.NODE_CONFIG_ENV,
+       NODE_CONFIG:             process.env.NODE_CONFIG,
+     };
      process.env.NODE_CONFIG_DIR         = import.meta.dirname + '/6-config';
      // ...
    });
+
+   afterEach(function() {
+     for (const [key, val] of Object.entries(origEnv)) {
+       if (val === undefined) delete process.env[key];
+       else process.env[key] = val;
+     }
+   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/6-strict-mode.js` around lines 77 - 97, Tests set environment variables
in beforeEach (process.env.NODE_CONFIG_DIR, NODE_CONFIG_STRICT_MODE,
NODE_APP_INSTANCE, NODE_ENV, NODE_CONFIG_ENV and delete process.env.NODE_CONFIG)
and never restore them; update the test to capture the original values before
mutation and add an afterEach that restores those originals (reassign saved
values or delete if undefined) and also clear any side-effects from
requireUncached/require (so the next test runs with a clean environment); locate
the setup in beforeEach and implement the teardown symmetrically in an afterEach
in the same test file surrounding the requireUncached call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/util.js`:
- Around line 1451-1467: The catch block in _loadParser currently logs errors
but falls through returning undefined, which then overwrites
DEFAULT_OPTIONS.parser and breaks callers like options.parser.getFilesOrder;
update the catch block inside function _loadParser to return the fallback
defaultParser (i.e., return defaultParser) after logging the failure and error,
leaving the existing Path.resolve(dir, name) and createRequire usage intact so
file-path usage remains supported.

In `@package.json`:
- Around line 48-50: Remove the self-referencing "config" entry from the
"dependencies" block in package.json and instead add an "exports" mapping so
local imports like "config" and "config/parser" resolve to local files; update
package.json to remove the dependency "config": "^4.3.0", add an "exports" field
that maps "." to the built entry (e.g. "./lib/config.js") and "./parser" to
"./parser.js", and keep the existing typesVersions mapping intact so
test/type-files/util.ts imports resolve to local sources during development and
type-checking.

In `@test/23-serialize.js`:
- Line 9: The test uses import.meta.dirname to set process.env.NODE_CONFIG_DIR,
but import.meta.dirname is only defined in Node ≥20.11.0 while package.json
currently declares ">=20.0.0"; update the minimum Node.js engine in package.json
to ">=20.11.0" (change the engines field) so import.meta.dirname will be
defined, and ensure any CI/test matrix or docs that advertise the Node
requirement are updated accordingly.

In `@test/6-strict-mode.js`:
- Line 79: The engines constraint in package.json allows Node versions where
import.meta.dirname is undefined, breaking tests that rely on
import.meta.dirname; update the "engines" field in package.json to require Node
>= 20.11.0 by changing the current "node": ">= 20.0.0" to "node": ">= 20.11.0"
so import.meta.dirname is guaranteed to exist for tests that use it (e.g., test
files referencing import.meta.dirname).

In `@test/8-config-extending.js`:
- Line 21: The test sets process.env.NODE_CONFIG_STRICT_MODE to a boolean which
becomes the string "false" and is inconsistent with other tests; change the
assignment so NODE_CONFIG_STRICT_MODE is set to the string '0' (i.e., replace
the boolean assignment to process.env.NODE_CONFIG_STRICT_MODE with the literal
string '0') to match convention used in tests like test/5-getConfigSources.js
and ensure consistent interpretation by the library.

In `@test/type-files/defer.ts`:
- Line 1: The import path in test/type-files/defer.ts is wrong: replace the
non-existent '../../types/lib/defer' with the actual module location
'../../lib/defer' so that the named imports deferConfig and DeferredConfig
resolve correctly; update the import statement to import { deferConfig,
DeferredConfig } from '../../lib/defer'.

In `@test/util.js`:
- Around line 43-53: The test calls to util.loadFileConfigs are passing a string
path which gets spread into the Load constructor options and produces numeric
keys instead of overriding configDir; update the two calls to loadFileConfigs in
test/util.js to pass an options object with the configDir property (e.g. {
configDir: path.join(import.meta.dirname, '5-config') }) so that Load's
this.options correctly overrides configDir and the test loads the intended
directory; reference symbols: util.loadFileConfigs, Load constructor,
this.options, configDir.

In `@test/x-deferred-configs-ts.js`:
- Line 20: The closing brace for the before callback is incorrectly placed on
the same line as the await call (CONFIG = await
requireUncached('./lib/config.mjs'); });); fix the formatting in the test by
moving the `});` that closes the `before` callback onto its own line so that the
`await requireUncached('./lib/config.mjs')` line ends with a semicolon and the
`});` appears on the next line; locate the statement referencing CONFIG and
requireUncached in the before block to apply this change.

---

Outside diff comments:
In `@lib/util.js`:
- Around line 1391-1398: The hostname resolution try/catch currently calls
require('os') (which fails under ESM) and gets swallowed; replace the dynamic
require with the already-imported OS module and call OS.hostname() to set
hostName when hostName is falsy, keeping the existing catch behavior (i.e., set
hostName = '' on error). Update the block that references require('os') so it
uses the imported OS symbol and OS.hostname() instead of require.
- Around line 456-491: resolveAsyncConfigs currently gathers Promise instances
in promises[] but never writes their resolved values back into the original
config structure; update it to track each promise's target location and assign
the resolved value back into the config (for object properties and array
entries) after awaiting Promise.all. Modify the traversal in _iterate to, when
encountering a Promise, push an object containing the promise and a setter
reference (e.g., { promise, parent, keyOrIndex }) or push the promise alongside
its target index in a separate array; after awaiting Promise.all, iterate
results and use the stored parent/keyOrIndex to set the resolved values into the
original config so config.get() returns resolved values. Ensure the logic
handles promises inside arrays and nested objects and preserves the existing
sort/traversal behavior in resolveAsyncConfigs.

In `@test/0-util.js`:
- Around line 1637-1645: The test "loads the values in left-right order"
declares a callback parameter `done` but runs synchronously and never calls it,
causing node:test to hang; fix by removing the `done` parameter from the test
function signature (so it runs as a synchronous test) or alternatively call
`done()` after the assertions; update the test that invokes util.loadFileConfigs
and the surrounding it(...) callback (the function containing the assertion for
config.db.name) to use the synchronous form (no `done`) or to call `done()`
explicitly.
- Around line 761-820: Tests use import.meta.dirname (seen in calls to
load.loadFile with Path.join(import.meta.dirname, ...)), which is only available
in Node >=20.11.0; update the package.json "engines" field from ">= 20.0.0" to
">= 20.11.0" so CI and consumers run a compatible runtime, and rerun tests to
confirm path resolution issues are resolved.

In `@test/1-protected-test.js`:
- Around line 27-45: The test's beforeEach uses process.argv.push(...) which
accumulates the NODE_CONFIG flag across tests; replace the push with an
assignment that sets process.argv to a clean array containing the NODE_CONFIG
entry (matching the pattern used in 2-config-test.js) so each call to
requireUncached('./lib/config.mjs') sees a consistent argv; update the
beforeEach block (the process.argv handling) to assign a fresh array (e.g., with
two leading entries and the '--NODE_CONFIG=...' string) instead of pushing.

In `@test/6-strict-mode.js`:
- Around line 74-97: The shared variable error is not reset at the start of the
beforeEach closure, which can leave stale state across the two it blocks; inside
the beforeEach function (the closure returned by _expectException), set error =
undefined (or null) as the first statement before modifying env and calling
requireUncached('./lib/config.mjs') so each test run starts with a clean error
state.

---

Nitpick comments:
In `@lib/config.mjs`:
- Around line 598-615: The code uses filename.match(env) inside the
load.options.nodeEnv loop which treats env as a regex and can misbehave for
values with regex metacharacters; change the check to perform a literal
substring or exact comparison instead — e.g., use filename.includes(env) or
escape env before using RegExp — so replace the filename.match(env) call in the
sourceFilenames.some callback (referencing sourceFilenames,
load.options.nodeEnv, filename.match and load.getEnv) with a safe literal
comparison and keep the existing _warnOrThrow calls unchanged.

In `@package.json`:
- Line 5: Add an "exports" field to package.json to provide conditional entry
points for ESM and CJS consumers instead of relying on the non-standard "module"
field; map the "import" condition to "./lib/config.mjs" (ESM) and the "require"
condition to "./lib/config.js" (CJS), and keep "main" as fallback if desired, so
Node's resolver uses the correct file for each module system while preserving
the existing "./lib/config.mjs" and "./lib/config.js" artifacts referenced by
"module" and "main".

In `@parser.js`:
- Around line 90-103: In Parser.tsParser replace the unnecessary optional
chaining on require by changing require?.extensions['.ts'] to
require.extensions['.ts'] so the defensive TS lazy-init logic is clearer; locate
the check inside the Parser.tsParser function and update that conditional
expression only (leave the rest of the TS lazy-init and TS.register call
intact).
- Around line 6-7: The createRequire call creating moduleRequire is using
Path.join(process.cwd(), 'node_modules') which forces an extra, unnecessary
lookup; change the moduleRequire createRequire argument to either
createRequire(process.cwd()) to search node_modules directly or provide a
concrete file inside node_modules (e.g., Path.join(process.cwd(),
'node_modules', 'package.json')) so resolution does not probe a non-existent
nested node_modules—update the moduleRequire declaration accordingly (symbols:
moduleRequire, require, createRequire, Path, process.cwd()).

In `@test/_utils/requireUncached.mjs`:
- Line 9: The current resolution uses process.cwd() which breaks if tests run
outside the repo root; change the resolver in requireUncached.mjs to resolve
moduleName relative to the caller's module URL instead of process.cwd().
Specifically, replace Path.resolve(process.cwd(), moduleName) with a resolution
based on import.meta.url (convert to a file path with URL.fileURLToPath and use
Path.dirname(...)) so that requireUncached resolves modules relative to the test
file that imported it; alternatively, document the cwd assumption if you prefer
not to change behavior.

In `@test/0-util.js`:
- Around line 1534-1544: Remove the entire commented-out test that references
the CJS-only API require.extensions (the block that checks existingHandler and
asserts require.extensions['.ts'] after calling util.loadFileConfigs with
import.meta.dirname and '.ts'); simply delete those commented lines since
require.extensions is unavailable in ESM and there is no ESM equivalent, leaving
no replacement code.

In `@test/11-regexp.js`:
- Around line 8-14: The test sets NODE_APP_INSTANCE='regexp' in beforeEach which
is not cleaned up; add an afterEach (or restore logic) to remove or restore
process.env.NODE_APP_INSTANCE after each test that calls
requireUncached('./lib/config.mjs') so the environment doesn't leak into other
suites; implement this cleanup in the same test file (pair with the existing
beforeEach) to either delete process.env.NODE_APP_INSTANCE or reset it to its
prior value.

In `@test/16-custom-parser.js`:
- Around line 78-80: The after hook in test/16-custom-parser.js is declared
async but only performs a synchronous delete of process.env.NODE_CONFIG_PARSER;
remove the unnecessary async qualifier from the anonymous function passed to
after(...) so the hook is a plain synchronous function.

In `@test/2-config-test.js`:
- Around line 62-147: The inner suites (Immutability, Configurations from
$NODE_CONFIG, Configurations from --NODE_CONFIG) call
requireUncached('./lib/config.mjs') without re-establishing environment,
implicitly depending on the outer "Test suite for node-config" setup; fix by
moving the shared env/argv initialization (NODE_CONFIG_DIR, NODE_ENV,
NODE_APP_INSTANCE, NODE_CONFIG, process.argv) into the outer describe's setup
(use before or beforeEach at the outer describe level) so every inner
requireUncached loads a deterministically configured environment, and add
matching cleanup (after or afterEach) to restore original env/argv; keep inner
beforeEach only calling requireUncached('./lib/config.mjs') so tests remain
isolated.

In `@test/21-date.js`:
- Around line 1-13: Replace the string concatenation for NODE_CONFIG_DIR with a
cross-platform path join: instead of using import.meta.dirname + '/config'
compute the directory from import.meta.url (via url.fileURLToPath and
path.dirname) and then use path.join(dir, 'config'); update the top of the test
to import/require the Node 'path' and 'url' helpers and keep the existing before
hook, CONFIG variable, and requireUncached('./lib/config.mjs') unchanged.

In `@test/5-getConfigSources.js`:
- Around line 10-26: Save the original process.argv at the start of the test
setup and restore it in a teardown hook so other tests aren't affected: in the
beforeEach (where process.argv is set to
["node","path/to/some.js","--NODE_CONFIG={}"]) capture the original value (e.g.
const originalArgv = process.argv.slice()) and add an after or afterEach hook
that resets process.argv = originalArgv; ensure this lives alongside the
existing requireUncached('./lib/config.mjs') / sources =
config.util.getConfigSources() setup so tests clean up after themselves.

In `@test/6-strict-mode.js`:
- Around line 77-97: Tests set environment variables in beforeEach
(process.env.NODE_CONFIG_DIR, NODE_CONFIG_STRICT_MODE, NODE_APP_INSTANCE,
NODE_ENV, NODE_CONFIG_ENV and delete process.env.NODE_CONFIG) and never restore
them; update the test to capture the original values before mutation and add an
afterEach that restores those originals (reassign saved values or delete if
undefined) and also clear any side-effects from requireUncached/require (so the
next test runs with a clean environment); locate the setup in beforeEach and
implement the teardown symmetrically in an afterEach in the same test file
surrounding the requireUncached call.

In `@test/8-config-extending.js`:
- Around line 27-36: The test mutates process.env.NODE_CONFIG_DIR inside the
it() block which is redundant because the same value is set in the before()
hook; remove the assignment to process.env.NODE_CONFIG_DIR from the
it('Extending a configuration...') test body (the line that sets
process.env.NODE_CONFIG_DIR = import.meta.dirname + '/8-config') so the test
relies on the before() setup, leaving the rest of the test (reading
base-config.json, calling CONFIG.util.attachProtoDeep, and invoking
Util.extendDeep) unchanged.

In `@test/9-raw-configs.js`:
- Around line 8-13: The closing brace and parenthesis for the before(async() =>
{ ... }) hook are placed on the same line as the CONFIG assignment, making the
formatting look collapsed; update the before hook around CONFIG = await
requireUncached('./lib/config.mjs') so the final `});` is on its own line (i.e.,
close the async function block and the before call on a separate line) to
restore proper readability and consistent formatting in test/9-raw-configs.js.

@jdmarshall jdmarshall force-pushed the imports branch 3 times, most recently from 4c86b63 to 1e40f7c Compare February 19, 2026 07:50
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
lib/util.js (1)

1391-1398: ⚠️ Potential issue | 🔴 Critical

require('os') is not defined in ESM scope — hostname detection silently fails.

lib/util.js is now an ES module (top-level import at line 8, export at line 1469). In ESM, __filename, __dirname, and require are not available; require() can be created via module.createRequire(). The bare require('os') on line 1393 will throw ReferenceError: require is not defined at runtime. Because the surrounding try/catch catches all errors and sets hostName = '', this failure is completely silent — every process launched without HOST/HOSTNAME env vars will get an empty string hostname instead of the OS hostname.

OS is already imported at line 10 (import OS from 'node:os'), so the fix is trivial:

🐛 Proposed fix
-    try {
-      if (!hostName) {
-        const OS = require('os');
-        hostName = OS.hostname();
-      }
-    } catch (e) {
-      hostName = '';
-    }
+    if (!hostName) {
+      hostName = OS.hostname();
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/util.js` around lines 1391 - 1398, The try/catch around hostname
detection uses a legacy require('os') call which fails in ESM; update the code
that sets hostName (the try block referencing require('os')) to use the
already-imported OS symbol (import OS from 'node:os') instead of calling
require, e.g. call OS.hostname() when hostName is falsy, and keep the minimal
error handling (or narrow the catch) so failures are not silently swallowed;
change only the require('os') usage inside the hostName assignment logic (the
try block that sets hostName = OS.hostname()).
package.json (2)

1-93: ⚠️ Potential issue | 🟠 Major

Missing "type": "module" — all ESM .js files will be treated as CommonJS without it.

Every .js file that uses import/export syntax (lib/util.js, test/makeImmutable-shared-refs.js, test/22-binary.js, test/16-config/parser/custom-*.js, etc.) will throw a SyntaxError because the default "type" value when the field is absent is "commonjs", meaning Node.js treats all .js files as CommonJS modules.

However, lib/config.js uses module.exports, which is invalid in an ESM package, creating a conflict. The standard resolution is:

🔧 Proposed fix
  "main": "./lib/config.js",
+ "type": "module",

And rename lib/config.jslib/config.cjs, update package.json:

- "main": "./lib/config.js",
+ "main": "./lib/config.cjs",

Alternatively, rename all ESM .js library and test files to .mjs (avoids changing the package type for consumers).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 1 - 93, The package.json lacks "type":"module",
causing .js files with ESM syntax to be parsed as CommonJS and conflicting with
lib/config.js using module.exports; add "type": "module" to package.json and
make lib/config.js a CommonJS file by renaming it to lib/config.cjs and update
the "main" field from "./lib/config.js" to "./lib/config.cjs" (keep "module":
"./lib/config.mjs" as-is), or alternatively convert all ESM .js files to .mjs
and keep package.json without "type" — pick the former solution and ensure
references to main/module (and any imports of lib/config.js) are updated to the
new filename.

73-75: ⚠️ Potential issue | 🟠 Major

Update engines.node to ">= 22.12.0" — current declaration is too broad.

The codebase requires Node.js 22.12.0 or higher due to three features:

  • require('./config.mjs').default requires Node.js 22.12.0+ (stable require() of ES modules without --experimental-require-module flag)
  • import.meta.dirname is used extensively but unavailable on Node.js 20.0.0–20.10.x
  • --no-experimental-strip-types flag in test script is a Node.js 22+ feature

Declaring >= 20.0.0 will cause ERR_REQUIRE_ESM errors on Node.js 20.x and 22.0–22.11.x.

Diff
-  "node": ">= 20.0.0"
+  "node": ">= 22.12.0"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 73 - 75, Update the package.json engines.node
field from ">= 20.0.0" to ">= 22.12.0" so the runtime requirement matches
features used (referenced symbols: engines.node in package.json), because the
codebase uses require('./config.mjs').default, import.meta.dirname, and the
--no-experimental-strip-types flag which require Node.js 22.12.0+. Change the
engines.node string accordingly and verify any CI or documentation that asserts
Node versions reflects the new minimum.
test/1-protected-test.js (1)

27-45: ⚠️ Potential issue | 🟡 Minor

process.argv.push in beforeEach accumulates duplicate entries across tests.

Each beforeEach invocation appends another --NODE_CONFIG=… entry to process.argv without ever removing previous ones. After N tests in this suite, argv will contain N copies. Contrast with test/2-config-test.js Line 30 which replaces process.argv entirely.

If the config parser iterates all argv entries, duplicate overrides are parsed on every fresh config load — identical values won't break anything today, but it's a latent bug if values ever diverge or if another test file runs in the same process afterward.

Suggested fix: replace argv instead of pushing
-    process.argv.push('--NODE_CONFIG={"EnvOverride":{"parm5":"overridden from --NODE_CONFIG","parm6":101}}');
+    process.argv = [process.argv[0], process.argv[1], '--NODE_CONFIG={"EnvOverride":{"parm5":"overridden from --NODE_CONFIG","parm6":101}}'];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/1-protected-test.js` around lines 27 - 45, The test's beforeEach mutates
process.argv by pushing a flag, causing duplicates across runs; instead, in
beforeEach (the function that calls requireUncached('./lib/config.mjs')) replace
process.argv with a fresh array that preserves the node/executable entries and
sets the single --NODE_CONFIG entry (or restore a saved original argv then
append the single flag), so each test starts with a deterministic argv and no
accumulated duplicates.
test/0-util.js (1)

1637-1644: ⚠️ Potential issue | 🟡 Minor

Misleading done parameter — leftover from Mocha-style test

In node:test, the first callback argument is always a TestContext object, not a done-callback. The test completes synchronously regardless of whether this parameter is invoked. The done name is a misleading holdover from the Mocha migration and should be removed.

🛠 Proposed fix
-    it('loads the values in left-right order', function(done) {
+    it('loads the values in left-right order', function() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/0-util.js` around lines 1637 - 1644, The test callback for the "loads
the values in left-right order" case incorrectly declares a Mocha-style done
parameter; update the it(...) callback signature to remove the done argument
(change function(done) { ... } to function() { ... } or to an arrow () => { ...
}) and remove any references to done() within that test; the code block to
change surrounds the it(...) declaration that calls util.loadFileConfigs and
asserts config.db.name.
🧹 Nitpick comments (9)
test/16-custom-parser.js (1)

9-13: First suite's beforeEach doesn't clear NODE_CONFIG_PARSER.

If a prior test file sets NODE_CONFIG_PARSER and it leaks, this sanity-check suite ("Using the default parser") would pick it up. Consider adding delete process.env.NODE_CONFIG_PARSER here for robustness.

    beforeEach(async function() {
      process.env.NODE_CONFIG_DIR = import.meta.dirname + '/16-config';
+     delete process.env.NODE_CONFIG_PARSER;

      config = await requireUncached('./lib/config.mjs');
    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/16-custom-parser.js` around lines 9 - 13, The beforeEach in the "Using
the default parser" suite should explicitly clear any leaked parser env var: add
a statement to delete process.env.NODE_CONFIG_PARSER at the start of the
beforeEach (before setting process.env.NODE_CONFIG_DIR and calling
requireUncached('./lib/config.mjs')) so the default parser is used reliably;
modify the beforeEach block in test/16-custom-parser.js accordingly (refer to
the beforeEach function and the NODE_CONFIG_PARSER environment variable).
parser.js (1)

90-102: Unnecessary optional chaining on require.

require is always defined (assigned on line 7 via createRequire). The ?. on require?.extensions is a no-op here.

-  if (require?.extensions['.ts'] === undefined) {
+  if (require.extensions['.ts'] === undefined) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@parser.js` around lines 90 - 102, The optional chaining on require in
Parser.tsParser is unnecessary because require is always defined (created
earlier via createRequire); remove the `?.` so the code uses
`require.extensions['.ts']` directly. Update the check in the Parser.tsParser
function that currently reads `require?.extensions['.ts'] === undefined` to use
`require.extensions['.ts'] === undefined`, ensuring the rest of the logic
(including TS loading via TS_DEP and TS.register) remains unchanged.
lib/config.mjs (2)

529-556: attachProtoDeep iterates with for...in over config objects.

for (const prop in toObject) enumerates inherited properties as well. If toObject has a prototype chain beyond Object.prototype (e.g., after Util.extendDeep), unexpected inherited keys could be traversed. A hasOwnProperty guard (or Object.keys) would be safer, though this appears to replicate pre-existing behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/config.mjs` around lines 529 - 556, attachProtoDeep currently uses "for
(const prop in toObject)" which iterates inherited properties; change this to
iterate only own keys (e.g., use Object.keys(toObject).forEach or guard with
Object.prototype.hasOwnProperty.call(toObject, prop)) so only own enumerable
properties get processed; update the loop in attachProtoDeep to skip inherited
properties while preserving the recursion (depth - 1) and the Util.isObject
check so sub-objects receive prototypes correctly.

84-107: get() freezes config on first access — consider documenting the side effect.

The first get() call makes the entire config immutable (unless ALLOW_CONFIG_MUTATIONS is set). This is a significant side effect that may surprise callers accessing a single property. A brief inline comment or JSDoc note about the freeze-on-first-read behavior would help maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/config.mjs` around lines 84 - 107, The config.get method has a surprising
side effect: on first call it freezes the config via
Object.isExtensible/this.util.makeImmutable unless
LOAD_SYMBOL.initParam('ALLOW_CONFIG_MUTATIONS', false) permits mutations; add a
brief JSDoc or inline comment above the get(property) implementation (or above
the class) explicitly documenting the "freeze-on-first-read" behavior, mention
the ALLOW_CONFIG_MUTATIONS flag and LOAD_SYMBOL interaction, and note that
subsequent calls cannot mutate the config so callers should read needed values
before relying on mutations.
test/5-getConfigSources.js (1)

90-104: Inconsistent indentation in the last describe block.

Lines 91–103 use 1-space indentation while the rest of the file uses standard 2/4-space indentation. Likely pre-existing, but now is a good time to normalize.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/5-getConfigSources.js` around lines 90 - 104, The describe block
starting with "describe('Files which return empty objects still end up in
getConfigSources()'" and its nested beforeEach should be re-indented to match
the project's indentation style (use the same 2/4-space indentation used
elsewhere in the file) so the block aligns with other tests; update the leading
spaces for the lines containing describe(...), let sources;, beforeEach(async
function () {, the body that sets process.env and calls
requireUncached('./lib/config.mjs'), and the closing braces so they follow the
file's standard indentation and keep existing code unchanged.
test/8-config-extending.js (1)

26-30: Redundant NODE_CONFIG_DIR reassignment inside the test.

Line 28 re-sets NODE_CONFIG_DIR to the same value already established in the before hook (Line 13). This is harmless but unnecessary — consider removing it to reduce noise.

Suggested fix
     it('Extending a configuration with another configuration should work without error', function () {
-      process.env.NODE_CONFIG_DIR = import.meta.dirname + '/8-config';
       let filename = path.join(process.env.NODE_CONFIG_DIR,'base-config.json');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/8-config-extending.js` around lines 26 - 30, Remove the redundant
reassignment of process.env.NODE_CONFIG_DIR inside the test; rely on the value
set in the before hook instead. In the test "Extending a configuration with
another configuration should work without error" delete the line that sets
process.env.NODE_CONFIG_DIR (the one right before building filename with
path.join) so the test uses the existing env from the before hook and keeps the
rest of the code (filename, base_config, FileSystem.readFileSync) unchanged.
test/6-strict-mode.js (1)

73-97: error is not reset to undefined at the start of beforeEach.

If requireUncached does not throw in a subsequent beforeEach run, a stale error from a prior run would leak into the next it block. In the current test structure this can't happen (same env ⇒ same throw/no-throw outcome per describe), but it's a latent fragility.

Suggested fix
     beforeEach(async function() {
+      error = undefined;
       // Change the configuration directory for testing
       process.env.NODE_CONFIG_DIR         = import.meta.dirname + '/6-config';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/6-strict-mode.js` around lines 73 - 97, The variable error in
_expectException() can carry a stale value between runs; inside the beforeEach
callback (the one that sets env and then calls
requireUncached('./lib/config.mjs')) reset error to undefined at the start of
that beforeEach so each test run starts with a clean state; update the
beforeEach in _expectException to set error = undefined before attempting the
requireUncached call.
test/2-config-test.js (1)

62-67: Implicit env-var dependency between sibling describe blocks.

The Immutability describe (and all subsequent sibling describes) relies on process.env.NODE_CONFIG_DIR, NODE_ENV, etc. having been set by the Library initialization beforeEach. This works because node:test runs sibling describes sequentially in-process, but it's fragile if tests are ever run in isolation or reordered.

This appears to be a pre-existing pattern carried over from the CJS version, so not blocking — just noting for awareness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/2-config-test.js` around lines 62 - 67, The Immutability describe block
has an implicit dependency on environment variables set by the sibling "Library
initialization" beforeEach; update the Immutability beforeEach that calls
requireUncached('./lib/config.mjs') to explicitly set and export the required
env vars (e.g., process.env.NODE_CONFIG_DIR, process.env.NODE_ENV) before
loading, and restore or remove them in an afterEach so the test is isolated from
other sibling describes; reference the "Immutability" describe, its beforeEach,
requireUncached('./lib/config.mjs'), and the sibling "Library initialization"
beforeEach when making the change.
test/0-util.js (1)

1187-1235: Prefer Path.join() over string concatenation for configDir paths

Lines 1187, 1198, 1214, 1235 (and again at 1696, 1703, 1710, 1724, 1733, 1742) use import.meta.dirname + '/config' while the rest of the file consistently uses Path.join(import.meta.dirname, ...). The concatenation hardcodes a POSIX separator, which is inconsistent and would misfire on Windows.

♻️ Proposed fix (representative — same pattern applies to lines 1696–1742)
-      let load = new Load({nodeEnv: 'production', configDir: import.meta.dirname + '/config'})
+      let load = new Load({nodeEnv: 'production', configDir: Path.join(import.meta.dirname, 'config')})
-      let load = new Load({configDir: import.meta.dirname + '/config'});
+      let load = new Load({configDir: Path.join(import.meta.dirname, 'config')});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/0-util.js` around lines 1187 - 1235, The tests instantiate Load with
configDir built via string concatenation (import.meta.dirname + '/config') which
hardcodes a POSIX separator; update each construction (e.g. the Load(...) calls
used in the tests that call load.loadCustomEnvVars()) to use
Path.join(import.meta.dirname, 'config') instead so path resolution is
cross-platform and consistent with the rest of the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/config.js`:
- Line 4: The current lib/config.js uses synchronous
require('./config.mjs').default which throws ERR_REQUIRE_ESM on Node.js 20.x;
change the module to load the ESM via dynamic import instead of require and
expose a compatible API (e.g., export a getConfig async function or export a
Promise) so callers can obtain the default export asynchronously. Specifically,
replace the direct require usage in lib/config.js with code that does
import('./config.mjs') and returns the module's default export (reference the
existing module.exports usage and the './config.mjs' target) so consumers call
await getConfig() or await the exported Promise; this avoids ERR_REQUIRE_ESM and
keeps behavior stable until/if you convert to ESM or rename to .cjs.

In `@test/0-util.js`:
- Around line 7-14: The package.json engines field currently states "node": ">=
20.0.0" but the codebase uses import.meta.dirname (used across the repo) which
requires Node.js 20.11.0 or newer; update package.json to set "engines": {
"node": ">= 20.11.0" } so CI and users running versions 20.0.0–20.10.x won't
break at runtime when code (e.g., tests like test/0-util.js and modules
referencing import.meta.dirname) relies on that backported feature.

In `@test/3-config/local.js`:
- Around line 1-22: The deferredOriginal property is placed at the top level but
must be nested inside the original object to match the default config and tests;
move the deferredOriginal: defer(function(cfg, original) { return original; })
entry into the existing original object (alongside original: defer(...)) so that
CONFIG.original.deferredOriginal is overridden correctly during merge and aligns
with the deferredOriginal in default.js and TypeScript equivalents.

In `@test/9-raw-configs.js`:
- Line 13: The closing brace/paren of the Mocha before hook is accidentally
appended to the same line as the await call (CONFIG = await
requireUncached('./lib/config.mjs');  });), making formatting wrong; edit the
before callback that contains CONFIG = await requireUncached('./lib/config.mjs')
so the closing "});" is moved to its own line (i.e., end the await line with a
semicolon and place the "}" and ")" on the following line), and apply the same
fix in the analogous case in test/x-deferred-configs-ts.js where the before
callback closing is appended to the await.

---

Outside diff comments:
In `@lib/util.js`:
- Around line 1391-1398: The try/catch around hostname detection uses a legacy
require('os') call which fails in ESM; update the code that sets hostName (the
try block referencing require('os')) to use the already-imported OS symbol
(import OS from 'node:os') instead of calling require, e.g. call OS.hostname()
when hostName is falsy, and keep the minimal error handling (or narrow the
catch) so failures are not silently swallowed; change only the require('os')
usage inside the hostName assignment logic (the try block that sets hostName =
OS.hostname()).

In `@package.json`:
- Around line 1-93: The package.json lacks "type":"module", causing .js files
with ESM syntax to be parsed as CommonJS and conflicting with lib/config.js
using module.exports; add "type": "module" to package.json and make
lib/config.js a CommonJS file by renaming it to lib/config.cjs and update the
"main" field from "./lib/config.js" to "./lib/config.cjs" (keep "module":
"./lib/config.mjs" as-is), or alternatively convert all ESM .js files to .mjs
and keep package.json without "type" — pick the former solution and ensure
references to main/module (and any imports of lib/config.js) are updated to the
new filename.
- Around line 73-75: Update the package.json engines.node field from ">= 20.0.0"
to ">= 22.12.0" so the runtime requirement matches features used (referenced
symbols: engines.node in package.json), because the codebase uses
require('./config.mjs').default, import.meta.dirname, and the
--no-experimental-strip-types flag which require Node.js 22.12.0+. Change the
engines.node string accordingly and verify any CI or documentation that asserts
Node versions reflects the new minimum.

In `@test/0-util.js`:
- Around line 1637-1644: The test callback for the "loads the values in
left-right order" case incorrectly declares a Mocha-style done parameter; update
the it(...) callback signature to remove the done argument (change
function(done) { ... } to function() { ... } or to an arrow () => { ... }) and
remove any references to done() within that test; the code block to change
surrounds the it(...) declaration that calls util.loadFileConfigs and asserts
config.db.name.

In `@test/1-protected-test.js`:
- Around line 27-45: The test's beforeEach mutates process.argv by pushing a
flag, causing duplicates across runs; instead, in beforeEach (the function that
calls requireUncached('./lib/config.mjs')) replace process.argv with a fresh
array that preserves the node/executable entries and sets the single
--NODE_CONFIG entry (or restore a saved original argv then append the single
flag), so each test starts with a deterministic argv and no accumulated
duplicates.

---

Duplicate comments:
In `@lib/util.js`:
- Around line 1451-1467: _loadParser currently swallows errors and returns
undefined if require fails; update the catch block in function _loadParser to
return defaultParser instead of falling through, and improve the logging to
include the caught error (use the caught variable e) so callers of _loadParser
always get a parser (defaultParser) when require(Path.resolve(dir, name)) in the
try of _loadParser fails.

In `@package.json`:
- Around line 48-50: The package.json currently declares a self-referencing
dependency "config": "^4.3.0" which duplicates the project itself; remove the
"config" entry from the "dependencies" block in package.json (and from any other
dependency lists) so the project does not depend on itself, then run your
package manager (npm install or yarn install) to update lockfiles and verify no
other references remain; look for the "config" string in the package.json and
ensure only external packages remain.

In `@test/8-config-extending.js`:
- Line 21: Replace the boolean assignment to the environment variable with a
string to match the test suite convention: change the statement setting
process.env.NODE_CONFIG_STRICT_MODE (in test/8-config-extending.js) from false
to the string '0' so the variable is consistently treated as a string like in
test/5-getConfigSources.js.

In `@test/type-files/defer.ts`:
- Line 1: The import statement for deferConfig and DeferredConfig in
test/type-files/defer.ts is using the duplicate path '../../types/lib/defer';
update this to the canonical import used elsewhere in the project (or remove the
duplicate and reuse the already-imported symbols) so there is only a single,
consistent import source for deferConfig and DeferredConfig; locate the import
statement referencing '../../types/lib/defer' and replace it with the project's
standard path or consolidate with the existing import of
deferConfig/DeferredConfig.

In `@test/x-deferred-configs-ts.js`:
- Line 20: The statement assigning CONFIG uses "CONFIG = await
requireUncached('./lib/config.mjs');  });" with the closing "});" on the same
line as the await expression; move the closing "});" onto its own properly
indented line so the await call ends on its own line (e.g., end the await
expression with a semicolon then place "});" on the next line) to satisfy the
formatting rule around requireUncached and CONFIG.

---

Nitpick comments:
In `@lib/config.mjs`:
- Around line 529-556: attachProtoDeep currently uses "for (const prop in
toObject)" which iterates inherited properties; change this to iterate only own
keys (e.g., use Object.keys(toObject).forEach or guard with
Object.prototype.hasOwnProperty.call(toObject, prop)) so only own enumerable
properties get processed; update the loop in attachProtoDeep to skip inherited
properties while preserving the recursion (depth - 1) and the Util.isObject
check so sub-objects receive prototypes correctly.
- Around line 84-107: The config.get method has a surprising side effect: on
first call it freezes the config via Object.isExtensible/this.util.makeImmutable
unless LOAD_SYMBOL.initParam('ALLOW_CONFIG_MUTATIONS', false) permits mutations;
add a brief JSDoc or inline comment above the get(property) implementation (or
above the class) explicitly documenting the "freeze-on-first-read" behavior,
mention the ALLOW_CONFIG_MUTATIONS flag and LOAD_SYMBOL interaction, and note
that subsequent calls cannot mutate the config so callers should read needed
values before relying on mutations.

In `@parser.js`:
- Around line 90-102: The optional chaining on require in Parser.tsParser is
unnecessary because require is always defined (created earlier via
createRequire); remove the `?.` so the code uses `require.extensions['.ts']`
directly. Update the check in the Parser.tsParser function that currently reads
`require?.extensions['.ts'] === undefined` to use `require.extensions['.ts'] ===
undefined`, ensuring the rest of the logic (including TS loading via TS_DEP and
TS.register) remains unchanged.

In `@test/0-util.js`:
- Around line 1187-1235: The tests instantiate Load with configDir built via
string concatenation (import.meta.dirname + '/config') which hardcodes a POSIX
separator; update each construction (e.g. the Load(...) calls used in the tests
that call load.loadCustomEnvVars()) to use Path.join(import.meta.dirname,
'config') instead so path resolution is cross-platform and consistent with the
rest of the file.

In `@test/16-custom-parser.js`:
- Around line 9-13: The beforeEach in the "Using the default parser" suite
should explicitly clear any leaked parser env var: add a statement to delete
process.env.NODE_CONFIG_PARSER at the start of the beforeEach (before setting
process.env.NODE_CONFIG_DIR and calling requireUncached('./lib/config.mjs')) so
the default parser is used reliably; modify the beforeEach block in
test/16-custom-parser.js accordingly (refer to the beforeEach function and the
NODE_CONFIG_PARSER environment variable).

In `@test/2-config-test.js`:
- Around line 62-67: The Immutability describe block has an implicit dependency
on environment variables set by the sibling "Library initialization" beforeEach;
update the Immutability beforeEach that calls
requireUncached('./lib/config.mjs') to explicitly set and export the required
env vars (e.g., process.env.NODE_CONFIG_DIR, process.env.NODE_ENV) before
loading, and restore or remove them in an afterEach so the test is isolated from
other sibling describes; reference the "Immutability" describe, its beforeEach,
requireUncached('./lib/config.mjs'), and the sibling "Library initialization"
beforeEach when making the change.

In `@test/5-getConfigSources.js`:
- Around line 90-104: The describe block starting with "describe('Files which
return empty objects still end up in getConfigSources()'" and its nested
beforeEach should be re-indented to match the project's indentation style (use
the same 2/4-space indentation used elsewhere in the file) so the block aligns
with other tests; update the leading spaces for the lines containing
describe(...), let sources;, beforeEach(async function () {, the body that sets
process.env and calls requireUncached('./lib/config.mjs'), and the closing
braces so they follow the file's standard indentation and keep existing code
unchanged.

In `@test/6-strict-mode.js`:
- Around line 73-97: The variable error in _expectException() can carry a stale
value between runs; inside the beforeEach callback (the one that sets env and
then calls requireUncached('./lib/config.mjs')) reset error to undefined at the
start of that beforeEach so each test run starts with a clean state; update the
beforeEach in _expectException to set error = undefined before attempting the
requireUncached call.

In `@test/8-config-extending.js`:
- Around line 26-30: Remove the redundant reassignment of
process.env.NODE_CONFIG_DIR inside the test; rely on the value set in the before
hook instead. In the test "Extending a configuration with another configuration
should work without error" delete the line that sets process.env.NODE_CONFIG_DIR
(the one right before building filename with path.join) so the test uses the
existing env from the before hook and keeps the rest of the code (filename,
base_config, FileSystem.readFileSync) unchanged.

@jdmarshall jdmarshall force-pushed the imports branch 4 times, most recently from 5b9b4d1 to 4ed70b9 Compare February 19, 2026 08:04
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
package.json (2)

75-81: ⚠️ Potential issue | 🟡 Minor

c8 coverage config misses .mjs files.

The include patterns ["*.js", "lib/**/*.js"] won't capture lib/config.mjs, which is now the primary runtime entry point. Coverage reports will undercount.

Proposed fix
   "c8": {
     "include": [
       "*.js",
-      "lib/**/*.js"
+      "lib/**/*.js",
+      "*.mjs",
+      "lib/**/*.mjs"
     ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 75 - 81, The c8 coverage "include" array currently
only matches .js files and therefore misses lib/config.mjs; update the c8
config's "include" array to also match .mjs files (e.g., add "*.mjs" and
"lib/**/*.mjs" or replace with patterns that include both .js and .mjs) so
lib/config.mjs is captured by coverage; refer to the c8 "include" array and the
lib/config.mjs entry point when making the change.

1-6: ⚠️ Potential issue | 🔴 Critical

Missing "type": "module" — ESM syntax in .js files will fail at runtime.

parser.js, lib/defer.js, and lib/util.js use top-level import/export syntax, but package.json does not declare "type": "module". Node.js defaults .js files to CommonJS, so these files will throw SyntaxError: Cannot use import statement in a module. Only lib/config.mjs (which uses the .mjs extension) would work correctly.

Add "type": "module" to enable ESM for all .js files, or rename the affected files to .mjs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 1 - 6, The package.json lacks the "type": "module"
field while files parser.js, lib/defer.js, and lib/util.js use ESM
imports/exports (only lib/config.mjs currently uses .mjs); add "type": "module"
to package.json to make .js files be treated as ESM, or alternatively rename the
ESM .js files (parser.js, lib/defer.js, lib/util.js) to .mjs so Node treats them
as modules, and keep lib/config.mjs unchanged.
test/0-util.js (1)

1637-1645: ⚠️ Potential issue | 🟠 Major

done callback declared but never called — test will hang/timeout.

function(done) tells node:test (and Mocha) to wait for done() to be invoked. Since the test body only runs synchronous assertions and never calls done(), it will timeout.

Proposed fix
-    it('loads the values in left-right order', function(done) {
+    it('loads the values in left-right order', function() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/0-util.js` around lines 1637 - 1645, The test "loads the values in
left-right order" declares a done callback but never calls it, causing the test
to hang; fix by making the test synchronous: remove the done parameter from the
it(...) callback (so it becomes function() { ... }) or alternatively call done()
after the synchronous assertion; locate the test case that calls
util.loadFileConfigs and assert.strictEqual(config.db.name, ...) and either
remove the unused done parameter or add a final done() invocation.
🧹 Nitpick comments (7)
test/_utils/requireUncached.mjs (1)

8-11: Cross-platform issue: appending a query string to a filesystem path may fail on Windows.

On Unix, import('/abs/path/file.mjs?c=0') works because Node.js treats /-prefixed specifiers as URL-like. On Windows, Path.resolve yields C:\repo\lib\config.mjs, and import('C:\\repo\\lib\\config.mjs?c=0') is not a valid URL — Node may try to open a file literally named config.mjs?c=0.

The robust approach is to use pathToFileURL:

♻️ Proposed fix for cross-platform cache-busting
-import Path from 'path';
+import Path from 'path';
+import { pathToFileURL } from 'node:url';

 let count = 0;

 export async function requireUncached(moduleName) {
   let path = Path.resolve(process.cwd(), moduleName);
-  let module = await import(`${path}?c=${count++}`);
+  let url = pathToFileURL(path);
+  url.searchParams.set('c', String(count++));
+  let module = await import(url.href);
   return module.default;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/_utils/requireUncached.mjs` around lines 8 - 11, The requireUncached
function currently builds a filesystem path via Path.resolve and appends a query
string to it before dynamic import, which breaks on Windows; change it to
convert the resolved path to a file:// URL using pathToFileURL (from the 'url'
module) and then append the cache-busting query parameter to that URL string
before calling import. Update requireUncached (and any uses of Path.resolve in
this function) to use pathToFileURL(resolvedPath).href + `?c=${count++}` so
imports are valid cross-platform.
parser.js (1)

90-115: require?.extensions — optional chaining is unnecessary here.

require (line 7) is assigned from createRequire(process.cwd()) and is always defined. The optional chaining require?.extensions is harmless but misleading — it suggests require might be nullish.

♻️ Minor clarity fix
-  if (require?.extensions['.ts'] === undefined) {
+  if (require.extensions['.ts'] === undefined) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@parser.js` around lines 90 - 115, In Parser.tsParser remove the unnecessary
optional chaining on require when checking extensions: replace the
`require?.extensions['.ts'] === undefined` check with a direct access
`require.extensions['.ts'] === undefined` (or a clearer explicit check like
`typeof require.extensions === 'undefined'`) so the code uses the known
non-nullable `require` provided by createRequire; update the conditional in
Parser.tsParser accordingly.
lib/config.mjs (1)

192-207: Potential stale reference: existing is read before the mutation, then extended in place.

Util.getPath (line 197) fetches the object reference after load.setModuleDefaults has already mutated load.config. Since the constructor did Util.extendDeep(this, load.config), this and load.config share different object trees. load.setModuleDefaults updates load.config but not this.#config directly. The branch at line 199 clones moduleConfig into this.#config only when the path doesn't exist yet; on subsequent calls (line 202), extendDeep(existing, moduleConfig) mutates the sub-object already on the config instance. This seems intentional but is subtle — worth a brief inline comment to clarify the ownership semantics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/config.mjs` around lines 192 - 207, The setModuleDefaults method
currently reads existing via Util.getPath(this.#config, path) after calling
load.setModuleDefaults(moduleName, defaultProperties), then mutates that
existing object in-place with Util.extendDeep(existing, moduleConfig); add a
brief inline comment in setModuleDefaults clarifying ownership and mutation
semantics (that load.setModuleDefaults updates load.config and that this.#config
only gets a cloned moduleConfig when the path is absent, otherwise existing is
mutated in-place), and ensure references to methods/fields in the comment
include setModuleDefaults, Util.getPath, load.setModuleDefaults,
Util.extendDeep, attachProtoDeep, this.#config and load.config to make the
intent explicit for future readers.
test/22-binary.js (1)

3-3: Buffer import is unnecessary — it's a Node.js global.

Buffer is available globally in Node.js without an explicit import. This import is harmless but redundant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/22-binary.js` at line 3, Remove the redundant import of the Node.js
global Buffer (the line "import { Buffer } from 'buffer';") in the
test/22-binary.js file; locate the import statement referencing Buffer and
delete it so the code uses the built-in global Buffer without an explicit
import.
test/2-config-test.js (1)

62-67: Env-var setup missing in "Immutability" beforeEach; relies on leakage from sibling describe.

The beforeEach at line 65 only loads config but doesn't set NODE_CONFIG_DIR, NODE_ENV, etc. This works because the "Library initialization" describe's beforeEach (line 18) sets these env vars on process.env and they persist globally. This is fragile — if test ordering changes or tests run in isolation, these tests will fail. Pre-existing concern, but worth noting during migration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/2-config-test.js` around lines 62 - 67, The Immutability describe's
beforeEach currently only calls requireUncached('./lib/config.mjs') and relies
on env vars set by a different describe; update this beforeEach to explicitly
set the required environment variables (e.g., NODE_CONFIG_DIR, NODE_ENV and any
others used by lib/config.mjs) before calling requireUncached so the test
doesn't depend on global leakage; you can either duplicate the env setup used in
the "Library initialization" beforeEach or call the shared helper that
initializes test env, then load requireUncached('./lib/config.mjs') to ensure
deterministic, isolated test setup.
test/0-util.js (1)

1459-1459: Eager loadFileConfigs call at describe-registration time.

Line 1459 executes util.loadFileConfigs(...) synchronously at describe scope rather than inside a beforeEach or before hook. This means it runs during test tree registration, before any setup hooks. It works here because loadFileConfigs is synchronous and doesn't depend on dynamic env state, but it's inconsistent with the hook-based pattern used elsewhere in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/0-util.js` at line 1459, The test eagerly calls
util.loadFileConfigs(...) at describe-registration time; move that synchronous
call into a setup hook (e.g., before or beforeEach) so it runs during test setup
rather than at import time. Change the top-level var config declaration to a
scoped let (e.g., let config) and inside a before/beforeEach assign config =
util.loadFileConfigs({configDir: Path.join(import.meta.dirname,
'config')}).config so tests keep the same value but follow the hook-based
pattern used elsewhere.
test/1-protected-test.js (1)

27-45: process.argv.push accumulates entries across beforeEach calls without cleanup.

Line 39 appends to process.argv on every beforeEach invocation but never removes the entry. Across multiple test iterations this grows process.argv unboundedly. Consider saving and restoring process.argv in an afterEach, or switching to before/after since the env setup is identical for all tests in this block.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/1-protected-test.js` around lines 27 - 45, The test's beforeEach handler
mutates process.argv with process.argv.push and never restores it, causing
accumulation across tests; update the setup to save the original argv (e.g.,
const originalArgv = process.argv.slice()) before modifying it and restore it in
an afterEach (process.argv = originalArgv), or move the entire setup into a
before/after pair so you only set process.argv once; change references in this
file's beforeEach/afterEach and ensure requireUncached('./lib/config.mjs') still
runs with the intended argv when needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/config.mjs`:
- Around line 680-690: The cmdLineConfig variable (from
load.getCmdLineArg('NODE_CONFIG')) must be initialized to {} before attempting
JSON.parse and reset to {} inside the catch so a malformed JSON string isn't
later pushed into additional and iterated as a string; update the code around
cmdLineConfig (same pattern used for envConfig) so that cmdLineConfig = {} is
set before the try and in the catch, then keep the additional.push({ name:
"--NODE_CONFIG argument", config: cmdLineConfig }) call as-is to ensure a safe
empty object is added on parse failure.

In `@parser.js`:
- Around line 1-7: Add "type": "module" to package.json so Node treats .js files
as ESM; this fixes runtime SyntaxError for top-level import/export used in
parser.js and lib/util.js (the files that use import/export and createRequire
usage). Open package.json and add the "type": "module" field at the top-level
JSON object, commit the change, and re-run the app to verify parser.js and
lib/util.js import syntax is accepted.

In `@test/type-files/parser.ts`:
- Line 1: The import in test/type-files/parser.ts points to a non-existent
module ('../../types/parser'); update the import to reference the actual
implementation at the repo root (use '../../parser' to load parser.js) or, if
you intended a type declaration, add a corresponding types/parser.d.ts and keep
the import; ensure the symbol name (Parser) matches the exported default from
parser.js or adjust to a named import accordingly.

---

Outside diff comments:
In `@package.json`:
- Around line 75-81: The c8 coverage "include" array currently only matches .js
files and therefore misses lib/config.mjs; update the c8 config's "include"
array to also match .mjs files (e.g., add "*.mjs" and "lib/**/*.mjs" or replace
with patterns that include both .js and .mjs) so lib/config.mjs is captured by
coverage; refer to the c8 "include" array and the lib/config.mjs entry point
when making the change.
- Around line 1-6: The package.json lacks the "type": "module" field while files
parser.js, lib/defer.js, and lib/util.js use ESM imports/exports (only
lib/config.mjs currently uses .mjs); add "type": "module" to package.json to
make .js files be treated as ESM, or alternatively rename the ESM .js files
(parser.js, lib/defer.js, lib/util.js) to .mjs so Node treats them as modules,
and keep lib/config.mjs unchanged.

In `@test/0-util.js`:
- Around line 1637-1645: The test "loads the values in left-right order"
declares a done callback but never calls it, causing the test to hang; fix by
making the test synchronous: remove the done parameter from the it(...) callback
(so it becomes function() { ... }) or alternatively call done() after the
synchronous assertion; locate the test case that calls util.loadFileConfigs and
assert.strictEqual(config.db.name, ...) and either remove the unused done
parameter or add a final done() invocation.

---

Duplicate comments:
In `@lib/config.js`:
- Around line 1-4: The current module uses require('./config.mjs').default which
throws ERR_REQUIRE_ESM on Node < 22.12; replace the synchronous require with a
dynamic import and export the loaded value as a Promise to avoid require-ing an
.mjs file. Specifically, change the module.exports assignment so it uses
import('./config.mjs').then(m => m.default) (i.e., set module.exports =
import('./config.mjs').then(m => m.default)) so consumers load the config
asynchronously instead of calling require('./config.mjs').default.

In `@lib/util.js`:
- Around line 1451-1467: The _loadParser function currently falls through to
return undefined on error (catch block) which can overwrite
DEFAULT_OPTIONS.parser; update the catch in _loadParser to log the error
(include e) and explicitly return defaultParser so failures don’t propagate
undefined, referencing the function name _loadParser, the variable defaultParser
and the catch block where require(Path.resolve(dir, name)) is attempted.

In `@package.json`:
- Around line 48-49: The package.json currently lists a dependency entry
"config": "^4.3.0" which causes npm to install the published package instead of
using the repo's local module; remove that dependency entry and instead expose
your local config module via package.json "exports" so local imports resolve to
the workspace source (e.g., add an "exports" mapping that points the package
import specifier for "./config" or the package name to the local file like
./src/config/index.js or the appropriate entry), ensure any internal imports use
the package name or exact export key you define, and update package.json's
"main" or "types" if needed so tools resolve the local module correctly.

In `@test/9-raw-configs.js`:
- Line 13: The closing brace/parenthesis for the before callback is incorrectly
appended to the await line; in the test file's before callback that calls CONFIG
= await requireUncached('./lib/config.mjs') you should move the closing sequence
"});" onto its own line (i.e., place the closing brace and closing parenthesis
on a separate line after the await statement) so the before callback's structure
is properly formatted and the await line ends with just the statement.

---

Nitpick comments:
In `@lib/config.mjs`:
- Around line 192-207: The setModuleDefaults method currently reads existing via
Util.getPath(this.#config, path) after calling
load.setModuleDefaults(moduleName, defaultProperties), then mutates that
existing object in-place with Util.extendDeep(existing, moduleConfig); add a
brief inline comment in setModuleDefaults clarifying ownership and mutation
semantics (that load.setModuleDefaults updates load.config and that this.#config
only gets a cloned moduleConfig when the path is absent, otherwise existing is
mutated in-place), and ensure references to methods/fields in the comment
include setModuleDefaults, Util.getPath, load.setModuleDefaults,
Util.extendDeep, attachProtoDeep, this.#config and load.config to make the
intent explicit for future readers.

In `@parser.js`:
- Around line 90-115: In Parser.tsParser remove the unnecessary optional
chaining on require when checking extensions: replace the
`require?.extensions['.ts'] === undefined` check with a direct access
`require.extensions['.ts'] === undefined` (or a clearer explicit check like
`typeof require.extensions === 'undefined'`) so the code uses the known
non-nullable `require` provided by createRequire; update the conditional in
Parser.tsParser accordingly.

In `@test/_utils/requireUncached.mjs`:
- Around line 8-11: The requireUncached function currently builds a filesystem
path via Path.resolve and appends a query string to it before dynamic import,
which breaks on Windows; change it to convert the resolved path to a file:// URL
using pathToFileURL (from the 'url' module) and then append the cache-busting
query parameter to that URL string before calling import. Update requireUncached
(and any uses of Path.resolve in this function) to use
pathToFileURL(resolvedPath).href + `?c=${count++}` so imports are valid
cross-platform.

In `@test/0-util.js`:
- Line 1459: The test eagerly calls util.loadFileConfigs(...) at
describe-registration time; move that synchronous call into a setup hook (e.g.,
before or beforeEach) so it runs during test setup rather than at import time.
Change the top-level var config declaration to a scoped let (e.g., let config)
and inside a before/beforeEach assign config = util.loadFileConfigs({configDir:
Path.join(import.meta.dirname, 'config')}).config so tests keep the same value
but follow the hook-based pattern used elsewhere.

In `@test/1-protected-test.js`:
- Around line 27-45: The test's beforeEach handler mutates process.argv with
process.argv.push and never restores it, causing accumulation across tests;
update the setup to save the original argv (e.g., const originalArgv =
process.argv.slice()) before modifying it and restore it in an afterEach
(process.argv = originalArgv), or move the entire setup into a before/after pair
so you only set process.argv once; change references in this file's
beforeEach/afterEach and ensure requireUncached('./lib/config.mjs') still runs
with the intended argv when needed.

In `@test/2-config-test.js`:
- Around line 62-67: The Immutability describe's beforeEach currently only calls
requireUncached('./lib/config.mjs') and relies on env vars set by a different
describe; update this beforeEach to explicitly set the required environment
variables (e.g., NODE_CONFIG_DIR, NODE_ENV and any others used by
lib/config.mjs) before calling requireUncached so the test doesn't depend on
global leakage; you can either duplicate the env setup used in the "Library
initialization" beforeEach or call the shared helper that initializes test env,
then load requireUncached('./lib/config.mjs') to ensure deterministic, isolated
test setup.

In `@test/22-binary.js`:
- Line 3: Remove the redundant import of the Node.js global Buffer (the line
"import { Buffer } from 'buffer';") in the test/22-binary.js file; locate the
import statement referencing Buffer and delete it so the code uses the built-in
global Buffer without an explicit import.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
test/0-util.js (1)

1637-1645: ⚠️ Potential issue | 🟡 Minor

Pre-existing: done callback declared but never called — will cause a timeout under node:test.

Line 1637 declares a done parameter in the test callback, but the test body (which now has a changed line at 1639) never calls it. Under node:test, this will cause the test to hang until it times out. Since this PR touches this test (updating the path), it's worth cleaning up:

-    it('loads the values in left-right order', function(done) {
+    it('loads the values in left-right order', function() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/0-util.js` around lines 1637 - 1645, The test callback for the "loads
the values in left-right order" it-block declares a done parameter but never
calls it, causing node:test to hang; remove the unused done parameter (or
alternatively call done() after the assertions) so the test is synchronous and
completes; locate the it(...) with the description "loads the values in
left-right order" in test/0-util.js and update its function signature to not
accept done (or add a done() invocation after assert.strictEqual) and ensure the
assertion remains unchanged.
parser.js (1)

90-115: ⚠️ Potential issue | 🟠 Major

Remove the unsupported lazy: true option from ts-node register call.

The project uses ts-node ^10.9.2. In ts-node v10 (and v9/v11), the register() method does not support a lazy option—modern ts-node already compiles on-demand by nature. The valid options here are transpileOnly, ignore, and compilerOptions, but lazy: true (line 95) is not recognized and should be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@parser.js` around lines 90 - 115, In Parser.tsParser, remove the unsupported
"lazy: true" option from the TS.register(...) call so ts-node is invoked with
only supported options (e.g., transpileOnly, ignore, compilerOptions); update
the TS.register invocation inside the TS === null branch (where TS is loaded via
moduleRequire(TS_DEP)) to omit the lazy property and keep the remaining options
intact.
test/16-config/parser/custom-3.js (1)

1-11: ⚠️ Potential issue | 🟡 Minor

Fix Parser isolation loss — static import creates singleton instead of fresh instance

custom-3.js now uses a direct static import for the Parser module, which returns the same ES module singleton across all tests. This breaks the original design that used requireUncached to get a fresh instance.

While the test likely still passes because custom-3.js calls setFilesOrder(['yaml', 'yml', 'json5']) which replaces the entire order array (preventing the 'custom' parser registered by custom-2.js from interfering), this is fragile. The 'custom' parser definition persists in the singleton and could cause unexpected behavior if:

  • Test execution order changes
  • Tests run in parallel
  • Future refactoring relies on isolation

Consider restoring isolation by using a dynamic import() with a cache-busting query parameter (as the test utility requireUncached does), or documenting the expectation that test isolation is maintained via setFilesOrder replacement rather than fresh instances.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/16-config/parser/custom-3.js` around lines 1 - 11, The test uses a
static import of Parser which returns the shared ES module singleton and breaks
test isolation; change custom-3.js to dynamically import a fresh Parser instance
(e.g., use import(...) with a cache-busting query param or the existing
requireUncached utility) instead of "import Parser from '../../../parser.js'",
then call Parser.setParser('json5', ...), setFilesOrder(['yaml','yml','json5'])
and export that fresh Parser so each test gets an isolated Parser instance
rather than the shared singleton.
🧹 Nitpick comments (6)
test/type-files/util.ts (1)

44-50: LGTM — one optional suggestion on resolved return type.

The overall pattern (static method access, call-signature check, assignability check, and negative @ts-expect-error) is well-structured and consistent with the rest of the file.

The only minor nit: resolved (line 46) has no type annotation, so .resolve()'s return type is exercised but not explicitly validated. If the return type is part of the public API contract you want to guard, consider:

♻️ Optional: annotate the resolved return type
-const resolved = rawConfig.resolve();
+const resolved: <ExpectedReturnType> = rawConfig.resolve();

This is strictly optional — the existing pattern matches how other results (e.g., loaded, envValue) are handled throughout the file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/type-files/util.ts` around lines 44 - 50, The variable resolved is
currently untyped so .resolve()'s return type isn't explicitly validated; update
the declaration of resolved to include the expected return type from
RawConfig.resolve (e.g., the library's ResolvedConfig/appropriate interface) so
the compiler enforces that .resolve() returns the intended type—locate
RawConfig.raw, the rawConfig variable, and the rawConfig.resolve() call and add
the explicit type annotation to resolved to match resolve()'s declared return
type.
test/_utils/requireUncached.mjs (1)

1-12: LGTM — clean ESM cache-busting utility.

The approach of appending a query parameter to force unique import URLs is the standard ESM cache-busting pattern. One minor note: on Windows, Path.resolve produces backslash paths (e.g., C:\foo\bar), and appending ?c=N to a plain filesystem path (rather than a file:// URL) may not behave as expected. If cross-platform test execution matters, consider using pathToFileURL from node:url:

Optional: more robust cross-platform version
 import Path from 'path';
+import { pathToFileURL } from 'node:url';

 let count = 0;

 export async function requireUncached(moduleName) {
   let path = Path.resolve(process.cwd(), moduleName);
-  let module = await import(`${path}?c=${count++}`);
+  let url = pathToFileURL(path);
+  url.searchParams.set('c', String(count++));
+  let module = await import(url.href);
   return module.default;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/_utils/requireUncached.mjs` around lines 1 - 12, The current
requireUncached uses Path.resolve to build a filesystem path and appends a query
param for ESM cache-busting, which can fail on Windows; change it to convert the
resolved path to a file:// URL using pathToFileURL from node:url and append the
?c=N there before calling dynamic import in requireUncached so the import target
is a proper URL across platforms (update references to Path.resolve and the
import(`${path}?c=${count++}`) call inside requireUncached).
lib/config.mjs (1)

1-15: Minor inconsistency: 'path' vs 'node:path' import.

Line 12 imports Path from 'path', while lib/util.js line 8 uses import Path from 'node:path'. The node: prefix is the recommended ESM convention for Node.js built-ins. Consider aligning for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/config.mjs` around lines 1 - 15, The import of Path in this module is
inconsistent with lib/util.js; update the module-level import statement that
declares Path (see the import currently: "import Path from 'path'") to use the
Node ESM built-in prefix so it matches util.js (i.e., import Path from
'node:path'), leaving other symbols like LOAD_SYMBOL and DEFAULT_CLONE_DEPTH
untouched.
parser.js (1)

6-7: Verify createRequire base paths are correct.

createRequire resolves modules relative to the dirname of the path you pass. For line 7, createRequire(process.cwd()) where cwd is e.g. /path/to/project means resolution starts from /path/to/ (the dirname), not from the project root. This doesn't matter here because require is only called with absolute paths (in jsParser, tsParser, coffeeParser), but it's worth noting the asymmetry with line 6.

Line 6's createRequire(Path.join(process.cwd(), 'node_modules')) correctly roots resolution at process.cwd() (since dirname of .../node_modules is the project root), making it find packages in <cwd>/node_modules/.

A more explicit approach for line 7:

-const require = createRequire(process.cwd());
+const require = createRequire(Path.join(process.cwd(), 'noop.js'));

Or simply reuse moduleRequire for config file loading since absolute paths resolve the same regardless of base.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@parser.js` around lines 6 - 7, The two createRequire calls are asymmetric and
may mis-root resolution; please stop creating require with process.cwd() and
instead reuse moduleRequire (the const moduleRequire created with
Path.join(process.cwd(), 'node_modules')) when loading config or other modules
(e.g., replace uses of the local require with moduleRequire in
jsParser/tsParser/coffeeParser config loads), or if you want a dedicated
require, create it from a path whose dirname is the project root (e.g.,
Path.join(process.cwd(), 'package.json')); update references to use the chosen
require to ensure consistent resolution.
test/8-config-extending.js (2)

2-2: Nit: unconventional import alias FileSystem.

The Node.js ecosystem overwhelmingly uses fs for this module. FileSystem diverges from convention and may confuse contributors.

Suggested fix
-import FileSystem from 'node:fs';
+import fs from 'node:fs';

Then update line 30:

-      let base_config = JSON.parse(FileSystem.readFileSync(filename, 'utf-8'));
+      let base_config = JSON.parse(fs.readFileSync(filename, 'utf-8'));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/8-config-extending.js` at line 2, Replace the unconventional import
alias FileSystem with the conventional fs: change the import statement "import
FileSystem from 'node:fs'" to import fs from 'node:fs' (or "import * as fs from
'node:fs'") and update all usages of the FileSystem identifier (e.g., the
reference on the line currently using FileSystem around line 30) to use fs
instead so the code follows Node.js conventions.

28-28: Redundant NODE_CONFIG_DIR assignment.

Line 13 in the before hook already sets NODE_CONFIG_DIR to the same value. This re-assignment inside the it block is unnecessary.

Suggested fix
-      process.env.NODE_CONFIG_DIR = import.meta.dirname + '/8-config';
       let filename = path.join(process.env.NODE_CONFIG_DIR,'base-config.json');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/8-config-extending.js` at line 28, The test contains a redundant
assignment to process.env.NODE_CONFIG_DIR inside the it block duplicating the
value already set in the before hook; remove the duplicate assignment in the it
block (the process.env.NODE_CONFIG_DIR = import.meta.dirname + '/8-config'
statement) so the before hook's configuration is the single source of setup for
NODE_CONFIG_DIR.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@parser.js`:
- Around line 377-378: The file exports the Parser class using ES module syntax
(export default Parser), so update project configuration: add "type": "module"
to package.json to ensure Node treats .js files as ES modules; locate
package.json and insert or update the top-level "type" field to "module" (or
alternatively convert the export to CommonJS require/module.exports if you
prefer not to set module type) so that Parser's export works at runtime.

In `@test/2-config-test.js`:
- Around line 65-67: The tests in sibling describe blocks call beforeEach that
only does config = await requireUncached('./lib/config.mjs') without resetting
environment, so NODE_CONFIG_DIR, NODE_ENV, NODE_CONFIG, etc. can leak and break
isolated runs; fix by moving the env setup into a shared top-level beforeEach
(or a top-level before) that sets NODE_CONFIG_DIR, NODE_ENV, NODE_CONFIG (and
any other test env vars) and then calls requireUncached('./lib/config.mjs') to
assign config, or alternatively repeat the same env-setting + requireUncached
call at the start of each sibling describe block so every beforeEach that calls
requireUncached runs with a known environment.

---

Outside diff comments:
In `@parser.js`:
- Around line 90-115: In Parser.tsParser, remove the unsupported "lazy: true"
option from the TS.register(...) call so ts-node is invoked with only supported
options (e.g., transpileOnly, ignore, compilerOptions); update the TS.register
invocation inside the TS === null branch (where TS is loaded via
moduleRequire(TS_DEP)) to omit the lazy property and keep the remaining options
intact.

In `@test/0-util.js`:
- Around line 1637-1645: The test callback for the "loads the values in
left-right order" it-block declares a done parameter but never calls it, causing
node:test to hang; remove the unused done parameter (or alternatively call
done() after the assertions) so the test is synchronous and completes; locate
the it(...) with the description "loads the values in left-right order" in
test/0-util.js and update its function signature to not accept done (or add a
done() invocation after assert.strictEqual) and ensure the assertion remains
unchanged.

In `@test/16-config/parser/custom-3.js`:
- Around line 1-11: The test uses a static import of Parser which returns the
shared ES module singleton and breaks test isolation; change custom-3.js to
dynamically import a fresh Parser instance (e.g., use import(...) with a
cache-busting query param or the existing requireUncached utility) instead of
"import Parser from '../../../parser.js'", then call Parser.setParser('json5',
...), setFilesOrder(['yaml','yml','json5']) and export that fresh Parser so each
test gets an isolated Parser instance rather than the shared singleton.

---

Duplicate comments:
In `@lib/config.js`:
- Line 4: The current line module.exports = require('./config.mjs').default
attempts to require an ESM file and triggers ERR_REQUIRE_ESM on Node 20+;
replace this direct require with a CJS-compatible approach: provide a true
CommonJS entry (e.g., create lib/config.cjs) that either (a) uses dynamic
import('./config.mjs') and assigns module.exports after the import resolves, or
(b) contains a CJS implementation/build of the config and export it
synchronously, and update package.json conditional "exports" so "require"
resolves to the .cjs file while ESM import still resolves to ./config.mjs;
remove the direct require('./config.mjs') in lib/config.js so no ESM file is
required from CommonJS.

In `@lib/config.mjs`:
- Around line 680-690: The try/catch around parsing the --NODE_CONFIG
command-line value should reset cmdLineConfig to an empty object on parse
failure to avoid pushing the original string into the config pipeline; in the
catch block for JSON.parse(cmdLineConfig) update cmdLineConfig = {} (and
optionally log the parse error), so that the subsequent additional.push({ name:
"--NODE_CONFIG argument", config: cmdLineConfig }) and downstream
load.scan/addConfig/Util.extendDeep receive an object rather than the malformed
string.

In `@test/x-deferred-configs-ts.js`:
- Line 20: The closing `});` of the `before` callback is incorrectly on the same
line as the await call; open the `before` callback body so that the assignment
line ends after the semicolon and move the `});` to its own line to properly
close the `before` block—look for the CONFIG = await
requireUncached('./lib/config.mjs') statement inside the before callback and
place the `});` on the next line.

---

Nitpick comments:
In `@lib/config.mjs`:
- Around line 1-15: The import of Path in this module is inconsistent with
lib/util.js; update the module-level import statement that declares Path (see
the import currently: "import Path from 'path'") to use the Node ESM built-in
prefix so it matches util.js (i.e., import Path from 'node:path'), leaving other
symbols like LOAD_SYMBOL and DEFAULT_CLONE_DEPTH untouched.

In `@parser.js`:
- Around line 6-7: The two createRequire calls are asymmetric and may mis-root
resolution; please stop creating require with process.cwd() and instead reuse
moduleRequire (the const moduleRequire created with Path.join(process.cwd(),
'node_modules')) when loading config or other modules (e.g., replace uses of the
local require with moduleRequire in jsParser/tsParser/coffeeParser config
loads), or if you want a dedicated require, create it from a path whose dirname
is the project root (e.g., Path.join(process.cwd(), 'package.json')); update
references to use the chosen require to ensure consistent resolution.

In `@test/_utils/requireUncached.mjs`:
- Around line 1-12: The current requireUncached uses Path.resolve to build a
filesystem path and appends a query param for ESM cache-busting, which can fail
on Windows; change it to convert the resolved path to a file:// URL using
pathToFileURL from node:url and append the ?c=N there before calling dynamic
import in requireUncached so the import target is a proper URL across platforms
(update references to Path.resolve and the import(`${path}?c=${count++}`) call
inside requireUncached).

In `@test/8-config-extending.js`:
- Line 2: Replace the unconventional import alias FileSystem with the
conventional fs: change the import statement "import FileSystem from 'node:fs'"
to import fs from 'node:fs' (or "import * as fs from 'node:fs'") and update all
usages of the FileSystem identifier (e.g., the reference on the line currently
using FileSystem around line 30) to use fs instead so the code follows Node.js
conventions.
- Line 28: The test contains a redundant assignment to
process.env.NODE_CONFIG_DIR inside the it block duplicating the value already
set in the before hook; remove the duplicate assignment in the it block (the
process.env.NODE_CONFIG_DIR = import.meta.dirname + '/8-config' statement) so
the before hook's configuration is the single source of setup for
NODE_CONFIG_DIR.

In `@test/type-files/util.ts`:
- Around line 44-50: The variable resolved is currently untyped so .resolve()'s
return type isn't explicitly validated; update the declaration of resolved to
include the expected return type from RawConfig.resolve (e.g., the library's
ResolvedConfig/appropriate interface) so the compiler enforces that .resolve()
returns the intended type—locate RawConfig.raw, the rawConfig variable, and the
rawConfig.resolve() call and add the explicit type annotation to resolved to
match resolve()'s declared return type.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
test/_utils/requireUncached.mjs (1)

8-11: Implementation is correct; document the default-only return behavior.

The cache-busting approach using query parameters is a valid pattern for ESM. Note that requireUncached only returns module.default, which works well for lib/config.mjs but would silently return undefined for modules without a default export.

Consider adding a brief JSDoc comment to clarify this expectation:

📝 Suggested documentation
+/**
+ * Dynamically import a module without caching.
+ * `@param` {string} moduleName - Path relative to process.cwd()
+ * `@returns` {Promise<any>} The module's default export
+ */
 export async function requireUncached(moduleName) {
   let path = Path.resolve(process.cwd(), moduleName);
   let module = await import(`${path}?c=${count++}`);
   return module.default;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/_utils/requireUncached.mjs` around lines 8 - 11, Add a short JSDoc above
the requireUncached function documenting that it performs an ESM import with a
cache-busting query and returns only the module's default export
(module.default), which will be undefined for modules that do not export a
default; update the comment to mention the expected use (e.g., config modules
with default export) so callers know this function intentionally returns
default-only.
test/6-strict-mode.js (1)

73-97: Consider resetting error at the start of beforeEach.

The error variable is declared at line 74 outside beforeEach, meaning it retains its value across test runs if the same test suite is re-executed. While this likely works correctly in practice, explicitly resetting error = undefined at the start of beforeEach would make the intent clearer and prevent potential issues.

   return () => {
     beforeEach(async function() {
+      error = undefined;
       // Change the configuration directory for testing
       process.env.NODE_CONFIG_DIR         = import.meta.dirname + '/6-config';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/6-strict-mode.js` around lines 73 - 97, In _expectException add an
explicit reset of the outer-scope variable error at the start of the beforeEach
block so previous test runs don't leak state; inside beforeEach (the function
that sets up process.env and calls requireUncached('./lib/config.mjs')) set
error = undefined before the try/catch so each test run begins with a clean
error value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/defer.js`:
- Line 1: The file lib/defer.js currently uses ESM syntax (import/export) but
the project package.json lacks "type":"module", so Node will treat .js as
CommonJS and this will fail; fix by either (A) renaming lib/defer.js to
lib/defer.mjs and keeping the ESM import (isAsyncFunction import at top) or (B)
adding "type":"module" to package.json so .js files are treated as ESM—ensure
any other files using ESM (noted at the second occurrence around line 69) are
updated consistently to match the chosen approach.

In `@parser.js`:
- Around line 6-7: The createRequire calls are anchored to directories which is
incorrect; change them to use a specific file path (e.g., package.json) so
module resolution has a proper filename context: update the moduleRequire
assignment that uses createRequire(Path.join(process.cwd(), 'node_modules')) to
instead join process.cwd() with 'package.json', and likewise update the require
assignment that uses createRequire(process.cwd()) to use a file path
(process.cwd() + '/package.json' or Path.join(process.cwd(), 'package.json')) so
createRequire, moduleRequire and require are invoked with a filename rather than
a directory.

---

Duplicate comments:
In `@lib/config.js`:
- Around line 1-4: The current synchronous assignment module.exports =
require('./config.mjs').default will throw ERR_REQUIRE_ESM on Node 20.x; replace
it with an async loader: remove the require(...) usage and export an async
function (e.g., module.exports = async function loadConfig() { const mod = await
import('./config.mjs'); return mod.default; }) and then update any callers to
await loadConfig(); reference the existing module.exports and
require('./config.mjs').default sites and change them to the new async
loadConfig function so ESM is loaded via dynamic import instead of synchronous
require.

In `@lib/util.js`:
- Around line 1450-1466: The _loadParser function currently logs errors but
returns undefined on failure (e.g., when NODE_CONFIG_PARSER points to an invalid
path), which can overwrite DEFAULT_OPTIONS.parser and break callers like
options.parser.getFilesOrder(); modify _loadParser so that on any catch it
returns defaultParser (not undefined) after logging the error, ensuring the
function (and callers relying on defaultParser) remain safe; locate the
_loadParser implementation and adjust the catch block to return defaultParser
(referencing defaultParser, createRequire, and Path.resolve in your changes).

In `@test/2-config-test.js`:
- Around line 65-67: The tests call requireUncached('./lib/config.mjs') inside
sibling beforeEach blocks but do not reset NODE_CONFIG_DIR, NODE_ENV,
NODE_CONFIG, etc., causing state leakage when running isolated tests; update the
test setup to ensure environment is deterministically reset before each test by
moving the env-var initialization (set NODE_CONFIG_DIR, NODE_ENV, NODE_CONFIG,
and any other config-related vars) into a shared top-level before or beforeEach
that runs for all describe blocks, or alternatively duplicate the same env
setup/reset in every sibling beforeEach that calls requireUncached, so
requireUncached always sees a clean, explicit environment.

In `@test/type-files/parser.ts`:
- Line 1: The import at the top of test/type-files/parser.ts ("import Parser
from '../../types/parser'") points to a non-existent types/parser module; either
update that import to reference the actual root parser implementation module
used at runtime (the parser.js module) so the resolved module exists, or
alternatively add a proper type declaration file named to provide the Parser
types (e.g., a types/parser.d.ts) and export the Parser type so the existing
import works; adjust the import or add the declaration accordingly in the
Parser-related symbols to restore TypeScript compilation.

In `@test/util.js`:
- Line 43: The test calls util.loadFileConfigs with a bare string; change it to
pass a LoadOptions object so the configDir is set correctly by calling
util.loadFileConfigs({configDir: path.join(import.meta.dirname, '5-config')});
update the other test occurrence similarly (the second call around line 50) so
both calls pass an object rather than a raw string, ensuring loadFileConfigs
receives the expected LoadOptions shape.

In `@test/x-deferred-configs-ts.js`:
- Line 20: Move the stray closing tokens off the same line as the await call: in
the before callback where you call CONFIG = await
requireUncached('./lib/config.mjs'); ensure the trailing `});` that closes the
before callback is placed on its own line (i.e., separate the await statement
from the callback closure) so the `await` and the callback terminator are not on
the same line.

---

Nitpick comments:
In `@test/_utils/requireUncached.mjs`:
- Around line 8-11: Add a short JSDoc above the requireUncached function
documenting that it performs an ESM import with a cache-busting query and
returns only the module's default export (module.default), which will be
undefined for modules that do not export a default; update the comment to
mention the expected use (e.g., config modules with default export) so callers
know this function intentionally returns default-only.

In `@test/6-strict-mode.js`:
- Around line 73-97: In _expectException add an explicit reset of the
outer-scope variable error at the start of the beforeEach block so previous test
runs don't leak state; inside beforeEach (the function that sets up process.env
and calls requireUncached('./lib/config.mjs')) set error = undefined before the
try/catch so each test run begins with a clean error value.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
benchmarks/index.js (1)

12-12: ⚠️ Potential issue | 🟠 Major

Update benchmarks/index.js to use dynamic import() instead of require() for lib/util.js.

lib/util.js uses ESM syntax (import/export), but package.json does not have "type": "module" configured. The require() calls at lines 12 and 56 will fail with a module loading error since CommonJS cannot load ESM syntax. Use dynamic import() to load the ESM module, or convert benchmarks/index.js to ESM as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@benchmarks/index.js` at line 12, Replace the CommonJS require() usage for the
ESM module by loading "../lib/util.js" via dynamic import() and await its
default/named exports; specifically change the require of "{ Load, Util }" (and
the second require at the later occurrence) to use an awaited import() so you
get the ESM exports, and if the file is not already using top-level await wrap
the import in an async IIFE (or convert the file to ESM) so you can destructure
the imported { Load, Util } and continue using those symbols unchanged.
test/1-protected-test.js (1)

27-45: ⚠️ Potential issue | 🟠 Major

process.argv.push in beforeEach without cleanup causes unbounded argv growth.

The old code captured argvOrg and restored it; removing that save/restore while keeping the push (line 39) means each of the 11 it() calls in this file triggers another append to process.argv. By the last test, process.argv contains 11 duplicate --NODE_CONFIG entries, polluting the global process state for every subsequent test file.

Additionally, beforeEach reloads lib/config.mjs 11 times for tests that are purely reading from config. Switching to before would suffice and eliminate the issue entirely.

🐛 Proposed fix — switch to `before` and reload once
-import { describe, it, beforeEach } from 'node:test';
+import { describe, it, before, after } from 'node:test';
 ...
-  beforeEach(async function () {
+  before(async function () {
     process.env.NODE_CONFIG_DIR = import.meta.dirname + '/config';
     process.env.NODE_ENV = 'test';
     process.env.NODE_APP_INSTANCE = '3';
     process.env.NODE_CONFIG = '{"EnvOverride":{"parm3":"overridden from $NODE_CONFIG","parm4":100}}';
     process.argv.push('--NODE_CONFIG={"EnvOverride":{"parm5":"overridden from --NODE_CONFIG","parm6":101}}');
     process.env['CUSTOM_JSON_ENVIRONMENT_VAR'] = 'CUSTOM VALUE FROM JSON ENV MAPPING';
     config = await requireUncached('./lib/config.mjs');
   });
+
+  after(function () {
+    const idx = process.argv.findIndex(a => a.startsWith('--NODE_CONFIG='));
+    if (idx !== -1) process.argv.splice(idx, 1);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/1-protected-test.js` around lines 27 - 45, The test setup currently
mutates process.argv inside beforeEach by calling process.argv.push(...) and
reloads lib/config.mjs repeatedly via requireUncached in beforeEach, causing
argv to grow across tests; change the hook to before so the NODE_CONFIG arg is
appended and lib/config.mjs is loaded only once (or if you must keep beforeEach,
capture a copy of process.argv (argvOrg) before pushing and restore it after
each test) — update the beforeEach -> before for the setup block that sets
process.env keys and calls process.argv.push and
requireUncached('./lib/config.mjs') (or implement argv save/restore around
process.argv.push) to prevent unbounded global state mutation.
package.json (1)

75-82: ⚠️ Potential issue | 🟠 Major

lib/config.mjs is excluded from coverage reporting.

The c8 include glob only matches *.js and lib/**/*.js. The new primary module lib/config.mjs will never appear in coverage output — any untested paths there are invisible.

🔧 Proposed fix
  "c8": {
    "include": [
      "*.js",
-     "lib/**/*.js"
+     "lib/**/*.js",
+     "lib/**/*.mjs"
    ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 75 - 82, The c8 config in package.json currently
only includes "*.js" and "lib/**/*.js", which excludes lib/config.mjs; update
the "c8" -> "include" globs to also match .mjs files (for example add "/*.mjs",
"lib/**/*.mjs" or a generic "lib/**/*.{js,mjs}") so lib/config.mjs is picked up
by coverage reporting; modify the package.json c8 include array accordingly to
reference lib/config.mjs coverage inclusion.
🧹 Nitpick comments (8)
test/_utils/requireUncached.mjs (1)

8-11: module.default assumption limits reusability.

requireUncached always returns module.default, silently returning undefined if the imported module has no default export. This works for lib/config.mjs but could cause confusing failures if reused elsewhere. Consider returning the full module namespace and letting callers destructure, or at minimum adding a guard.

💡 Optional: return full module or guard
 export async function requireUncached(moduleName) {
   let path = Path.resolve(process.cwd(), moduleName);
   let module = await import(`${path}?c=${count++}`);
-  return module.default;
+  return module.default ?? module;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/_utils/requireUncached.mjs` around lines 8 - 11, The helper
requireUncached currently returns only module.default which can be undefined for
modules without a default export; update the requireUncached function to return
the full imported module namespace (the imported module object) so callers can
destructure what they need, or alternatively add a guard inside requireUncached
that throws or returns the module namespace when module.default is undefined;
locate the requireUncached function and change its return behavior accordingly
so it no longer silently returns undefined for non-default exports.
test/8-config-extending.js (1)

28-30: Redundant NODE_CONFIG_DIR reassignment inside the test body.

Line 13 in the before hook already sets this to the same value. The reassignment on line 28 is a no-op.

♻️ Proposed fix
-      process.env.NODE_CONFIG_DIR = import.meta.dirname + '/8-config';
-      let filename = path.join(process.env.NODE_CONFIG_DIR,'base-config.json');
+      let filename = path.join(import.meta.dirname, '8-config', 'base-config.json');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/8-config-extending.js` around lines 28 - 30, Remove the redundant
reassignment of process.env.NODE_CONFIG_DIR inside the test body since the
before hook already sets it; locate the code that sets
process.env.NODE_CONFIG_DIR (the before hook and the line in the test that does
process.env.NODE_CONFIG_DIR = import.meta.dirname + '/8-config') and delete the
latter assignment so only the before hook establishes NODE_CONFIG_DIR, leaving
the FileSystem.readFileSync call and the rest of the test unchanged.
test/22-binary.js (1)

3-3: Buffer is a Node.js global — the import is redundant.

Buffer is available as a global in all Node.js environments and does not need to be imported from 'buffer'.

♻️ Proposed fix
-import { Buffer } from 'buffer';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/22-binary.js` at line 3, Remove the redundant import of Buffer from
'buffer' — delete the line "import { Buffer } from 'buffer';" (the code uses the
global Buffer, so no import is necessary) and run tests to confirm no other
module relies on that named import; if any code explicitly expects a locally
imported Buffer symbol, replace references to use the global Buffer instead.
test/6-strict-mode.js (1)

73-97: error is not reset before each beforeEach run — fragile if behavior differs across it blocks.

error is declared once per _expectException closure (line 74) and set only in the catch. If a future refactor adds a test where beforeEach does not throw, error will still hold the stale value from a prior run. Consider resetting error = undefined at the top of beforeEach for resilience.

💡 Suggested defensive reset
     beforeEach(async function() {
+      error = undefined;
       // Change the configuration directory for testing
       process.env.NODE_CONFIG_DIR         = import.meta.dirname + '/6-config';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/6-strict-mode.js` around lines 73 - 97, In function _expectException,
reset the closure-scoped variable error at the start of the beforeEach block to
avoid carrying a stale error between test runs; inside the beforeEach (before
the try/catch) set error = undefined so each test begins with a clean state when
requireUncached('./lib/config.mjs') runs and any thrown exception is freshly
captured.
test/2-config-test.js (1)

41-44: Assigning numbers to process.env silently coerces to strings.

process.env.CUSTOM_NUMBER_INTEGER_ENVIRONMENT_VAR = 1001 stores "1001" (a string). This works but is misleading — consider using string literals for clarity and to match what consumers actually receive.

💡 Suggested clarification
-      process.env.CUSTOM_NUMBER_INTEGER_ENVIRONMENT_VAR = 1001;
-      process.env.CUSTOM_NUMBER_FLOAT_ENVIRONMENT_VAR = 3.14;
+      process.env.CUSTOM_NUMBER_INTEGER_ENVIRONMENT_VAR = '1001';
+      process.env.CUSTOM_NUMBER_FLOAT_ENVIRONMENT_VAR = '3.14';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/2-config-test.js` around lines 41 - 44, The test assigns numeric and
other non-string values directly to process.env (e.g.,
process.env.CUSTOM_NUMBER_INTEGER_ENVIRONMENT_VAR,
process.env.CUSTOM_NUMBER_FLOAT_ENVIRONMENT_VAR,
process.env.CUSTOM_NUMBER_EMPTY_ENVIRONMENT_VAR,
process.env.CUSTOM_NUMBER_STRING_ENVIRONMENT_VAR) which are implicitly coerced
to strings; change these assignments to explicit string literals (e.g., "1001",
"3.14", "", "String") so the test reflects the actual string values consumers
receive and avoids misleading numeric assignments.
lib/config.mjs (2)

14-15: DEFAULT_CLONE_DEPTH is duplicated from lib/util.js.

This constant is also defined in lib/util.js (line 77) with the same value. If it ever changes in one place but not the other, behavior would silently diverge. Consider importing it from util.js or exporting it from a shared location.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/config.mjs` around lines 14 - 15, DEFAULT_CLONE_DEPTH is duplicated
between lib/config.mjs and lib/util.js; remove the duplicate by centralizing the
constant (either export DEFAULT_CLONE_DEPTH from lib/util.js or create a shared
constants module) and import it into lib/config.mjs instead of redefining it.
Update references in lib/config.mjs to use the imported DEFAULT_CLONE_DEPTH
(leave LOAD_SYMBOL untouched) and ensure lib/util.js (or the new shared module)
exports DEFAULT_CLONE_DEPTH so all callers use the single source of truth.

529-545: Known limitation: config keys named get, has, or util shadow prototype methods.

The if (!toObject[fnName]) check (line 542) skips attachment when a truthy config property exists with the same name. This silently breaks prototype method access for those sub-objects. This is a known pre-existing bug per project learnings, so flagging for awareness only.

Based on learnings: "In node-config lib/config.js, config keys named 'get', 'has', or 'util' can silently shadow prototype methods in attachProtoDeep because the check if (!toObject[fnName]) skips attachment when a truthy property exists."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/config.mjs` around lines 529 - 545, attachProtoDeep currently skips
adding hidden prototype methods when a config sub-object has a truthy key named
'get'/'has'/'util'/LOAD_SYMBOL, which lets those keys silently shadow the
methods; update the loop in attachProtoDeep so it detects owned properties with
Object.prototype.hasOwnProperty.call(toObject, fnName) and then always call
Util.makeHidden(toObject, fnName, this.#config[fnName]) when the existing
property is not a function (i.e., change the condition to attach if the prop is
missing or its typeof !== 'function'), referencing attachProtoDeep,
Util.makeHidden, and LOAD_SYMBOL so named keys no longer block provisioning the
method behaviors.
parser.js (1)

90-103: tsParser re-registers ts-node on every call when .ts extension is not yet set.

When require?.extensions['.ts'] is undefined, the parser loads and calls TS.register() — but only guards against repeated registration with if (TS === null). Once registered, require.extensions['.ts'] may still be undefined depending on ts-node's lazy mode, causing repeated entry into the guarded block (which is fine due to the null check). The logic is correct but the outer condition (require?.extensions['.ts'] === undefined) and the inner guard (TS === null) overlap in a confusing way.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@parser.js` around lines 90 - 103, The current logic in Parser.tsParser
redundantly checks require?.extensions['.ts'] === undefined before guarding
registration with TS === null, which is confusing and can lead to repeated
conditional evaluation; change the flow to first check the module-level TS
variable (if TS === null) and only then load and call TS.register(), and after
successful registration ensure the `.ts` loader is present (e.g., set or rely on
require.extensions['.ts'] if needed) so repeated calls to Parser.tsParser no
longer re-enter the registration path; update references in this function
(Parser.tsParser, TS, and TS.register) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/util.js`:
- Line 1455: The call to createRequire in _loadParser uses dir which may be
relative (coming from fromEnvironment / NODE_CONFIG_DIR); normalize dir to an
absolute path (or file URL) before passing to createRequire—e.g., use the
existing _toAbsolutePath helper (or path.resolve) to convert dir to an absolute
path inside _loadParser so createRequire receives a valid absolute path/file
URL; update the code around const require = createRequire(dir) accordingly and
keep behavior consistent with loadFileConfigs.

In `@test/16-custom-parser.js`:
- Around line 45-50: Make the parser paths consistent by either adding or
removing the `.js` extension; update the uses of process.env.NODE_CONFIG_PARSER
in the beforeEach blocks (and the similar occurrence around lines referencing
custom-3) so that `custom-2` and `custom-3` include the `.js` extension to match
`custom-1.js` (or alternatively remove the extension from `custom-1.js` to match
the others) — change the string values assigned to
process.env.NODE_CONFIG_PARSER and any other test assignments so all three
parser names use the same `.js` extension convention.

In `@test/22-binary.js`:
- Around line 10-16: The test sets NODE_CONFIG_DIR, NODE_ENV, and
NODE_APP_INSTANCE in the beforeEach but never cleans them up; add an afterEach
that restores the original values saved before mutation (or deletes the env var
if it was undefined) to avoid leaking into other tests—capture originals at the
start of beforeEach (or in an outer scope) and implement the cleanup in an
afterEach next to beforeEach in the same test file (referencing the existing
beforeEach and requireUncached usage to locate where to save/restore).

---

Outside diff comments:
In `@benchmarks/index.js`:
- Line 12: Replace the CommonJS require() usage for the ESM module by loading
"../lib/util.js" via dynamic import() and await its default/named exports;
specifically change the require of "{ Load, Util }" (and the second require at
the later occurrence) to use an awaited import() so you get the ESM exports, and
if the file is not already using top-level await wrap the import in an async
IIFE (or convert the file to ESM) so you can destructure the imported { Load,
Util } and continue using those symbols unchanged.

In `@package.json`:
- Around line 75-82: The c8 config in package.json currently only includes
"*.js" and "lib/**/*.js", which excludes lib/config.mjs; update the "c8" ->
"include" globs to also match .mjs files (for example add "/*.mjs",
"lib/**/*.mjs" or a generic "lib/**/*.{js,mjs}") so lib/config.mjs is picked up
by coverage reporting; modify the package.json c8 include array accordingly to
reference lib/config.mjs coverage inclusion.

In `@test/1-protected-test.js`:
- Around line 27-45: The test setup currently mutates process.argv inside
beforeEach by calling process.argv.push(...) and reloads lib/config.mjs
repeatedly via requireUncached in beforeEach, causing argv to grow across tests;
change the hook to before so the NODE_CONFIG arg is appended and lib/config.mjs
is loaded only once (or if you must keep beforeEach, capture a copy of
process.argv (argvOrg) before pushing and restore it after each test) — update
the beforeEach -> before for the setup block that sets process.env keys and
calls process.argv.push and requireUncached('./lib/config.mjs') (or implement
argv save/restore around process.argv.push) to prevent unbounded global state
mutation.

---

Duplicate comments:
In `@lib/config.js`:
- Line 4: Replace the direct require of an .mjs file (module.exports =
require('./config.mjs').default) because require('./config.mjs') throws on
Node.js <22.12; instead change lib/config.js to require a CommonJS export first
(require('./config.cjs').default) and, if that file is missing, fall back to
dynamically importing the ESM file via
import(pathToFileURL(require.resolve('./config.mjs')).href) and export its
default; update the module.exports assignment to use the CommonJS require when
available and asynchronously load the .mjs only as a fallback so Node
20.x/22.0–22.11 users aren’t broken (refer to the module.exports assignment in
lib/config.js to locate the change).

In `@lib/util.js`:
- Around line 1450-1466: The catch in _loadParser currently logs errors but
falls through to return undefined, which can overwrite DEFAULT_OPTIONS.parser
via the fromEnvironment spread and later cause a crash in
options.parser.getFilesOrder(); modify _loadParser to return the fallback
defaultParser when require() fails (or rethrow) so that callers like
fromEnvironment retain a valid parser; specifically update the catch in function
_loadParser to return defaultParser (using the existing defaultParser symbol)
instead of returning undefined.

In `@package.json`:
- Line 5: The package.json currently adds a non-standard "module" field but
lacks a proper Node.js "exports" map; add an "exports" field that mirrors the
entry points (e.g., main, module, and subpath exports) so runtime subpath
imports like "config/parser" and "config/lib/defer" resolve correctly alongside
your existing typesVersions. Update package.json to include an exports object
that exposes the package root and the required internal subpaths, mapping them
to their corresponding ESM ("./lib/config.mjs") and CJS ("./lib/config.cjs" or
"./lib/config.js") files as appropriate, and ensure the default export and
conditional exports (import/require/types) are covered.

In `@parser.js`:
- Around line 377-378: The file uses an ESM-style "export default" for the
Parser symbol in parser.js while the project isn't declared as ESM; replace the
ambiguous export with a clear CommonJS export to avoid node's CJS/ESM fallback
(e.g., remove "export default Parser" and export Parser via module.exports) so
consumers and Node resolve consistently; alternatively, if you intend ESM,
ensure package.json has "type": "module" or rename the file to .mjs and update
imports accordingly.
- Around line 6-7: The second createRequire call is passing a directory
(process.cwd()) instead of a filename; update the require initialization so both
createRequire calls are anchored to a file path (e.g., use
Path.join(process.cwd(), 'package.json') or reuse the already-created
moduleRequire) by changing the argument passed to createRequire for the variable
require (or simply assign require = moduleRequire) so createRequire receives a
filename consistently.

In `@test/2-config-test.js`:
- Around line 62-67: The Immutability describe block (and its siblings) rely on
environment variables that are only set in the "Library initialization"
beforeEach, causing implicit ordering dependency; update the tests so env setup
is deterministic by moving the environment initialization (setting
NODE_CONFIG_DIR, NODE_ENV, NODE_APP_INSTANCE, NODE_CONFIG, process.argv and
CUSTOM_* vars) into a top-level beforeEach that runs before all sibling describe
blocks, or alternatively repeat that env reset at the start of each sibling
beforeEach (the one that calls requireUncached('./lib/config.mjs')) so
requireUncached always runs with the expected environment.

In `@test/type-files/parser.ts`:
- Line 1: The import path '../../types/parser' in the file referencing the
symbol Parser is unresolved; either add a types declaration (e.g., create
types/parser.d.ts that exports the Parser type) or update the import to the
correct existing module path that exports Parser (or adjust tsconfig paths to
map 'types/*' to the correct folder). Locate the import statement that reads
"import Parser from '../../types/parser'" and either (A) add a declaration file
exporting Parser (matching the expected default/export shape), or (B) change the
import to the actual file that defines Parser, and re-run TypeScript to confirm
the error is gone.

In `@test/util.js`:
- Around line 43-53: The test is passing a raw string into loadFileConfigs which
still delegates to the Load constructor that expects an options object
(configDir), so update the test call to pass an object like {configDir:
path.join(import.meta.dirname, '5-config')} instead of a plain string; locate
the call in the test util.js and change the two occurrences that call
util.loadFileConfigs(...) to pass the options object so they map to the Load
constructor's expected configDir parameter (symbols: loadFileConfigs, Load,
configDir).

In `@test/x-deferred-configs-ts.js`:
- Line 20: The closing of the before callback is on the same line as the await
call; move the trailing closing sequence "});" off the await line so that the
await expression "CONFIG = await requireUncached('./lib/config.mjs');" ends on
its own line and the callback closure "});" sits on the following line (this
affects the before callback surrounding CONFIG and requireUncached), preserving
indentation and semicolon placement.

---

Nitpick comments:
In `@lib/config.mjs`:
- Around line 14-15: DEFAULT_CLONE_DEPTH is duplicated between lib/config.mjs
and lib/util.js; remove the duplicate by centralizing the constant (either
export DEFAULT_CLONE_DEPTH from lib/util.js or create a shared constants module)
and import it into lib/config.mjs instead of redefining it. Update references in
lib/config.mjs to use the imported DEFAULT_CLONE_DEPTH (leave LOAD_SYMBOL
untouched) and ensure lib/util.js (or the new shared module) exports
DEFAULT_CLONE_DEPTH so all callers use the single source of truth.
- Around line 529-545: attachProtoDeep currently skips adding hidden prototype
methods when a config sub-object has a truthy key named
'get'/'has'/'util'/LOAD_SYMBOL, which lets those keys silently shadow the
methods; update the loop in attachProtoDeep so it detects owned properties with
Object.prototype.hasOwnProperty.call(toObject, fnName) and then always call
Util.makeHidden(toObject, fnName, this.#config[fnName]) when the existing
property is not a function (i.e., change the condition to attach if the prop is
missing or its typeof !== 'function'), referencing attachProtoDeep,
Util.makeHidden, and LOAD_SYMBOL so named keys no longer block provisioning the
method behaviors.

In `@parser.js`:
- Around line 90-103: The current logic in Parser.tsParser redundantly checks
require?.extensions['.ts'] === undefined before guarding registration with TS
=== null, which is confusing and can lead to repeated conditional evaluation;
change the flow to first check the module-level TS variable (if TS === null) and
only then load and call TS.register(), and after successful registration ensure
the `.ts` loader is present (e.g., set or rely on require.extensions['.ts'] if
needed) so repeated calls to Parser.tsParser no longer re-enter the registration
path; update references in this function (Parser.tsParser, TS, and TS.register)
accordingly.

In `@test/_utils/requireUncached.mjs`:
- Around line 8-11: The helper requireUncached currently returns only
module.default which can be undefined for modules without a default export;
update the requireUncached function to return the full imported module namespace
(the imported module object) so callers can destructure what they need, or
alternatively add a guard inside requireUncached that throws or returns the
module namespace when module.default is undefined; locate the requireUncached
function and change its return behavior accordingly so it no longer silently
returns undefined for non-default exports.

In `@test/2-config-test.js`:
- Around line 41-44: The test assigns numeric and other non-string values
directly to process.env (e.g.,
process.env.CUSTOM_NUMBER_INTEGER_ENVIRONMENT_VAR,
process.env.CUSTOM_NUMBER_FLOAT_ENVIRONMENT_VAR,
process.env.CUSTOM_NUMBER_EMPTY_ENVIRONMENT_VAR,
process.env.CUSTOM_NUMBER_STRING_ENVIRONMENT_VAR) which are implicitly coerced
to strings; change these assignments to explicit string literals (e.g., "1001",
"3.14", "", "String") so the test reflects the actual string values consumers
receive and avoids misleading numeric assignments.

In `@test/22-binary.js`:
- Line 3: Remove the redundant import of Buffer from 'buffer' — delete the line
"import { Buffer } from 'buffer';" (the code uses the global Buffer, so no
import is necessary) and run tests to confirm no other module relies on that
named import; if any code explicitly expects a locally imported Buffer symbol,
replace references to use the global Buffer instead.

In `@test/6-strict-mode.js`:
- Around line 73-97: In function _expectException, reset the closure-scoped
variable error at the start of the beforeEach block to avoid carrying a stale
error between test runs; inside the beforeEach (before the try/catch) set error
= undefined so each test begins with a clean state when
requireUncached('./lib/config.mjs') runs and any thrown exception is freshly
captured.

In `@test/8-config-extending.js`:
- Around line 28-30: Remove the redundant reassignment of
process.env.NODE_CONFIG_DIR inside the test body since the before hook already
sets it; locate the code that sets process.env.NODE_CONFIG_DIR (the before hook
and the line in the test that does process.env.NODE_CONFIG_DIR =
import.meta.dirname + '/8-config') and delete the latter assignment so only the
before hook establishes NODE_CONFIG_DIR, leaving the FileSystem.readFileSync
call and the rest of the test unchanged.

@jdmarshall jdmarshall force-pushed the imports branch 2 times, most recently from a691d9e to ab50588 Compare February 23, 2026 03:42
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
test/6-strict-mode.js (1)

77-97: ⚠️ Potential issue | 🟠 Major

Clear env vars you don’t set in _expectException to prevent cross-case leakage.

In the beforeEach (Line 77-97), NODE_ENV / NODE_CONFIG_ENV are only set if provided, but never deleted otherwise. Since this helper creates many sibling describes in one file, a prior case can leave one of these set and influence later cases.

Proposed adjustment
     beforeEach(async function() {
       // Change the configuration directory for testing
       process.env.NODE_CONFIG_DIR         = import.meta.dirname + '/6-config';
       process.env.NODE_CONFIG_STRICT_MODE = 1;
       process.env.NODE_APP_INSTANCE       = opts.APP_INSTANCE;
 
+      delete process.env.NODE_ENV;
+      delete process.env.NODE_CONFIG_ENV;
+
       if (!!opts.NODE_ENV) {
         process.env.NODE_ENV              = opts.NODE_ENV;
       }
 
       if (!!opts.NODE_CONFIG_ENV) {
         process.env.NODE_CONFIG_ENV       = opts.NODE_CONFIG_ENV;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/6-strict-mode.js` around lines 77 - 97, In the beforeEach hook that sets
up test env for requireUncached('./lib/config.mjs'), ensure you clear NODE_ENV
and NODE_CONFIG_ENV when opts doesn't provide them to avoid cross-case leakage:
modify the beforeEach logic (the same block that currently sets
process.env.NODE_ENV and process.env.NODE_CONFIG_ENV based on opts) to
explicitly delete process.env.NODE_ENV and process.env.NODE_CONFIG_ENV in the
else path (so when !!opts.NODE_ENV or !!opts.NODE_CONFIG_ENV is false), leaving
the existing delete process.env.NODE_CONFIG and requireUncached usage intact.
parser.js (1)

151-158: ⚠️ Potential issue | 🟡 Minor

icedParser returns undefined — it only registers the compiler but never loads the config file.

The function sets up Iced and calls register(), but unlike coffeeParser (which ends with return require(filename)), this function has no return statement. Any .iced config file will parse to undefined. This appears to be a pre-existing bug (not introduced by this PR), but worth noting since this code was touched.

Likely missing return
 Parser.icedParser = function(filename, content) {
-  Iced = moduleRequire(ICED_DEP);
+  if (!Iced) {
+    Iced = moduleRequire(ICED_DEP);
+  }
 
   // coffee-script >= 1.7.0 requires explicit registration for require() to work
   if (Iced.register) {
     Iced.register();
   }
+  return require(filename);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@parser.js` around lines 151 - 158, Parser.icedParser currently only registers
the Iced compiler and never returns the loaded config, causing .iced files to
parse to undefined; update Parser.icedParser to, after calling Iced.register(),
require and return the parsed module for the given filename (mirror the behavior
of coffeeParser), ensuring the function returns the result of require(filename)
so callers receive the loaded config.
test/1-protected-test.js (1)

27-45: ⚠️ Potential issue | 🟡 Minor

process.argv.push in beforeEach accumulates duplicate --NODE_CONFIG entries across tests.

Line 39 pushes a --NODE_CONFIG argument into process.argv on every beforeEach invocation. Since node:test runs the outer beforeEach for each it block (including nested ones), process.argv grows with duplicate entries throughout the suite. While getCmdLineArg returns on the first match so results are correct, consider cleaning up in an afterEach or switching to before if per-test isolation isn't needed.

Option A: Clean up argv
+  import { describe, it, beforeEach, afterEach } from 'node:test';
   ...
+  afterEach(function() {
+    const idx = process.argv.indexOf('--NODE_CONFIG={"EnvOverride":{"parm5":"overridden from --NODE_CONFIG","parm6":101}}');
+    if (idx !== -1) process.argv.splice(idx, 1);
+  });
Option B: Use `before` instead of `beforeEach`

If tests don't need per-test config isolation, switch the outer hook to before so the push only happens once.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/1-protected-test.js` around lines 27 - 45, The test hook currently
pushes a persistent CLI arg via process.argv.push('--NODE_CONFIG=...') inside
beforeEach, causing duplicates across tests; fix by either moving that push into
a before hook (so it runs once) or by adding an afterEach that removes the
argument (e.g., find the last index of the exact '--NODE_CONFIG=...' string and
splice it out) to restore process.argv; update the hook surrounding the call to
process.argv.push (the beforeEach block that calls
requireUncached('./lib/config.mjs')) accordingly and ensure any cleanup runs
before/after requireUncached to preserve test isolation.
test/16-config/parser/custom-3.js (1)

1-11: ⚠️ Potential issue | 🔴 Critical

Parser configuration files cannot be loaded via synchronous require().

These files use ESM import/export syntax (lines 1 and 11), but _loadParser() in lib/util.js loads them via synchronous require() (line 1464). Node.js does not support loading ES modules with export via require(), even with createRequire(). This will fail at runtime.

Convert to CommonJS (module.exports), or refactor _loadParser() to use async import().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/16-config/parser/custom-3.js` around lines 1 - 11, The parser config
file uses ESM import/export (Parser.setParser in custom-3.js and default export)
but the loader function _loadParser in lib/util.js uses synchronous require(),
which breaks for ES modules; fix by converting this config file to CommonJS
(replace import/export with require() and module.exports and keep
Parser.setParser/Parser.setFilesOrder calls) OR update _loadParser to
dynamically import ES modules using async import() (detect ESM by package/type
or file extension and await import(path), then read exported default or
module.exports) and adjust callers to handle the async load; reference
Parser.setParser, Parser.setFilesOrder in the config and _loadParser in
lib/util.js when making the change.
🧹 Nitpick comments (8)
test/16-custom-parser.js (1)

78-80: Restore the prior NODE_CONFIG_PARSER value instead of unconditional delete.

Unconditionally deleting in after() can surprise other tests running in the same process (or local debugging). Capturing the previous value at file scope and restoring it is more robust.

Proposed tweak
 describe('Tests for a custom parser provided by NODE_CONFIG_PARSER', function() {
+  const prevParser = process.env.NODE_CONFIG_PARSER;
+
   ...
 
   after(async function () {
-    delete process.env.NODE_CONFIG_PARSER;
+    if (prevParser === undefined) delete process.env.NODE_CONFIG_PARSER;
+    else process.env.NODE_CONFIG_PARSER = prevParser;
   });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/16-custom-parser.js` around lines 78 - 80, The after() hook currently
deletes process.env.NODE_CONFIG_PARSER unconditionally; capture the prior value
at file scope (e.g., const OLD_NODE_CONFIG_PARSER =
process.env.NODE_CONFIG_PARSER) before tests run and in the after(async function
() { ... }) restore it instead of deleting: if the captured value is undefined
delete the env var, otherwise set process.env.NODE_CONFIG_PARSER =
OLD_NODE_CONFIG_PARSER. Update references around the existing after(...) block
and the top of the test file where NODE_CONFIG_PARSER is touched.
test/0-util.js (1)

1187-1199: Prefer Path.join(import.meta.dirname, ...) over string concatenation for configDir.

There are multiple places building configDir via import.meta.dirname + '/config' (Line 1187, Line 1199, Line 1215, etc.) while other call sites correctly use Path.join(...). Mixing separators is usually fine, but this is easy to make fully consistent.

Proposed adjustment (pattern)
- let load = new Load({nodeEnv: 'production', configDir: import.meta.dirname + '/config'})
+ let load = new Load({nodeEnv: 'production', configDir: Path.join(import.meta.dirname, 'config')})

Also applies to: 1696-1711, 1724-1744

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/0-util.js` around lines 1187 - 1199, The tests construct configDir using
string concatenation with import.meta.dirname (e.g., in the Load instantiation
calls around Load and load.loadCustomEnvVars), which is inconsistent with other
call sites that use Path.join; update those Load({nodeEnv: ..., configDir:
import.meta.dirname + '/config'}) usages to build the path with
Path.join(import.meta.dirname, 'config') (and similarly for any other
concatenations noted) so path separators are handled consistently across
platforms.
test/makeImmutable-shared-refs.js (1)

1-5: Consider importing Util directly to reduce env-sensitive coupling in this test.

const util = Config.util makes this test depend on ../lib/config.mjs singleton initialization (and whatever env/argv it reads) even though the assertions only need makeImmutable. If other tests leave NODE_CONFIG_* vars behind, this file can become order-dependent.

If the intent is “public API only”, keep as-is; otherwise consider importing { Util } from ../lib/util.js here and using that instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/makeImmutable-shared-refs.js` around lines 1 - 5, Replace the
env-sensitive indirect access via Config.util by importing the utility module
directly: remove the const util = Config.util line and instead import { Util }
(or the specific exported makeImmutable) from '../lib/util.js' and use
Util.makeImmutable (or the imported makeImmutable) in the tests; this replaces
references to Config and the Config.util symbol with direct references to Util
or makeImmutable to avoid depending on Config's singleton initialization.
test/x-config-ts/default.ts (1)

3-44: Add minimal typing for bootstrap({ defer }) to avoid implicit-any/implicit-this TS breakage.

If the repo’s TS settings get stricter, function bootstrap({ defer }) will likely trip noImplicitAny. You can keep it minimal and local to the fixture.

Proposed tweak
 import { Config } from './types';
 
-function bootstrap({ defer }) {
+type DeferFn = (fn: any) => any;
+
+function bootstrap({ defer }: { defer: DeferFn }): Config {
   const defaultConfig: Config = {
     siteTitle: 'Site title',
     latitude: 1,
     longitude: 2,
     arrayWithNull: [null],
   };
@@
   return defaultConfig;
 }
 
 export default bootstrap;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/x-config-ts/default.ts` around lines 3 - 44, Add explicit TypeScript
types to the bootstrap signature to avoid implicit-any/implicit-this: change
function bootstrap({ defer }) to annotate the destructured param and the return
type (e.g. function bootstrap({ defer }: { defer: any }): Config) so the
compiler knows defer's type and the function returns Config; ensure the Config
type reference remains and adjust to a tighter defer signature later if desired.
test/_utils/requireUncached.mjs (1)

8-11: module.default assumption limits reusability.

This utility always returns module.default, which works for the current use case (lib/config.mjs exports a default). If it's ever used with a module that only has named exports, the caller will silently get undefined. Consider returning module.default ?? module for resilience, or at minimum document the expectation.

Suggested improvement
 export async function requireUncached(moduleName) {
   let path = Path.resolve(process.cwd(), moduleName);
   let module = await import(`${path}?c=${count++}`);
-  return module.default;
+  return module.default ?? module;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/_utils/requireUncached.mjs` around lines 8 - 11, The helper
requireUncached currently always returns module.default which yields undefined
for modules that only export named members; update the function requireUncached
to return the default export if present otherwise the whole module (e.g., return
module.default ?? module) so callers receive either the default or the module
object, ensuring resilience across both default and named-export modules.
lib/config.mjs (1)

14-15: DEFAULT_CLONE_DEPTH is duplicated from lib/util.js.

This constant is also defined at lib/util.js:78. Having it in two places risks divergence.

Consider importing from util.js

Export the constant from lib/util.js and import it here, or move it to a shared constants module.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/config.mjs` around lines 14 - 15, DEFAULT_CLONE_DEPTH is duplicated in
lib/config.mjs and lib/util.js; remove the duplicate and import the canonical
value instead: export DEFAULT_CLONE_DEPTH from lib/util.js (or move it to a new
shared constants module) and replace the local const DEFAULT_CLONE_DEPTH in
lib/config.mjs with an import of that exported symbol (keep LOAD_SYMBOL
unchanged).
test/8-config-extending.js (1)

28-29: Redundant NODE_CONFIG_DIR assignment inside test body.

Line 13 in before() already sets process.env.NODE_CONFIG_DIR to import.meta.dirname + '/8-config'. Line 28 assigns the exact same value again inside the test, making it dead code.

♻️ Suggested cleanup
   it('Extending a configuration with another configuration should work without error', function () {
-    process.env.NODE_CONFIG_DIR = import.meta.dirname + '/8-config';
-    let filename = path.join(process.env.NODE_CONFIG_DIR,'base-config.json');
+    let filename = path.join(import.meta.dirname, '8-config', 'base-config.json');
     let base_config = JSON.parse(FileSystem.readFileSync(filename, 'utf-8'));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/8-config-extending.js` around lines 28 - 29, The assignment to
process.env.NODE_CONFIG_DIR inside the test is redundant because before()
already sets it; remove the duplicate assignment on the test lines (the second
process.env.NODE_CONFIG_DIR = import.meta.dirname + '/8-config') so the test
uses the value established by before() and avoid dead code while keeping the
existing filename = path.join(process.env.NODE_CONFIG_DIR,'base-config.json')
reference intact.
test/11-regexp.js (1)

8-14: beforeEach reloads config before every test unnecessarily — prefer before.

All five tests in this suite share identical environment settings and none of them mutate config. Using beforeEach causes five async module re-imports where one would suffice. Every other migrated test file in this PR (test/21-date.js, test/8-config-extending.js) correctly uses before for the same pattern.

♻️ Suggested change
-import { describe, it, beforeEach } from 'node:test';
+import { describe, it, before } from 'node:test';
 import assert from 'assert';
 import { requireUncached } from './_utils/requireUncached.mjs';

 describe('Tests for regexp', function() {
   let config;

-  beforeEach(async function() {
+  before(async function() {
     process.env.NODE_CONFIG_DIR = import.meta.dirname + '/config';
     process.env.NODE_ENV = 'test';
     process.env.NODE_APP_INSTANCE = 'regexp';

     config = await requireUncached('./lib/config.mjs');
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/11-regexp.js` around lines 8 - 14, The test setup uses beforeEach to set
env and reload config (process.env assignments and config = await
requireUncached('./lib/config.mjs')) which causes unnecessary repeated async
imports; change beforeEach to before so the environment variables
(import.meta.dirname + '/config', NODE_ENV, NODE_APP_INSTANCE) and the single
requireUncached('./lib/config.mjs') call run once for the suite. Update the hook
name from beforeEach to before in this file (referencing beforeEach, before,
config, requireUncached, import.meta.dirname) and ensure the async function
signature is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/16-custom-parser.js`:
- Around line 9-13: The beforeEach block must force-disable any custom parser by
clearing NODE_CONFIG_PARSER before loading the module: add a line to delete
process.env.NODE_CONFIG_PARSER (or set it to undefined/empty) at the start of
beforeEach (the same block that sets NODE_CONFIG_DIR and calls
requireUncached('./lib/config.mjs')), so the default parser sanity check always
runs regardless of the developer’s environment or previous tests.

In `@test/2-config-test.js`:
- Line 3: The requireUncached implementation should build a file:// URL instead
of interpolating a filesystem path into import(); in
test/_utils/requireUncached.mjs replace Path.resolve(...) string interpolation
with pathToFileURL(fsPath) (imported from 'node:url'), append a cache-busting
search param (e.g. url.searchParams.set('c', String(count++))) and call
import(url.href) to load the module; keep the exported function name
requireUncached and the incrementing count logic so Windows paths like C:\...
become valid file URLs.

---

Outside diff comments:
In `@parser.js`:
- Around line 151-158: Parser.icedParser currently only registers the Iced
compiler and never returns the loaded config, causing .iced files to parse to
undefined; update Parser.icedParser to, after calling Iced.register(), require
and return the parsed module for the given filename (mirror the behavior of
coffeeParser), ensuring the function returns the result of require(filename) so
callers receive the loaded config.

In `@test/1-protected-test.js`:
- Around line 27-45: The test hook currently pushes a persistent CLI arg via
process.argv.push('--NODE_CONFIG=...') inside beforeEach, causing duplicates
across tests; fix by either moving that push into a before hook (so it runs
once) or by adding an afterEach that removes the argument (e.g., find the last
index of the exact '--NODE_CONFIG=...' string and splice it out) to restore
process.argv; update the hook surrounding the call to process.argv.push (the
beforeEach block that calls requireUncached('./lib/config.mjs')) accordingly and
ensure any cleanup runs before/after requireUncached to preserve test isolation.

In `@test/16-config/parser/custom-3.js`:
- Around line 1-11: The parser config file uses ESM import/export
(Parser.setParser in custom-3.js and default export) but the loader function
_loadParser in lib/util.js uses synchronous require(), which breaks for ES
modules; fix by converting this config file to CommonJS (replace import/export
with require() and module.exports and keep Parser.setParser/Parser.setFilesOrder
calls) OR update _loadParser to dynamically import ES modules using async
import() (detect ESM by package/type or file extension and await import(path),
then read exported default or module.exports) and adjust callers to handle the
async load; reference Parser.setParser, Parser.setFilesOrder in the config and
_loadParser in lib/util.js when making the change.

In `@test/6-strict-mode.js`:
- Around line 77-97: In the beforeEach hook that sets up test env for
requireUncached('./lib/config.mjs'), ensure you clear NODE_ENV and
NODE_CONFIG_ENV when opts doesn't provide them to avoid cross-case leakage:
modify the beforeEach logic (the same block that currently sets
process.env.NODE_ENV and process.env.NODE_CONFIG_ENV based on opts) to
explicitly delete process.env.NODE_ENV and process.env.NODE_CONFIG_ENV in the
else path (so when !!opts.NODE_ENV or !!opts.NODE_CONFIG_ENV is false), leaving
the existing delete process.env.NODE_CONFIG and requireUncached usage intact.

---

Duplicate comments:
In `@lib/config.js`:
- Line 4: The current CommonJS file lib/config.js uses
require('./config.mjs').default which causes ERR_REQUIRE_ESM on Node 20+;
replace the synchronous require with a dynamic import or provide a CJS wrapper:
update lib/config.js to export a Promise that resolves to the ES module default
via import('./config.mjs').then(m => m.default) (or alternatively rename/create
a CJS module export that re-exports the default from config.mjs as a synchronous
CommonJS export by converting config.mjs to a .cjs file); locate the offending
expression require('./config.mjs').default and change it to use
import('./config.mjs').then(m => m.default) or move the implementation into a
.cjs file so consumers no longer call require() on an .mjs file.

In `@lib/defer.js`:
- Line 1: The file uses ESM-style import "import { isAsyncFunction } from
'node:util/types';" in a .js file without "type":"module"; replace that import
with a CommonJS require (const { isAsyncFunction } = require('node:util/types'))
or alternatively convert the file to .mjs or set "type":"module" in
package.json; update lib/defer.js to use the chosen module style and ensure any
usages of isAsyncFunction continue to work with the new import form.

In `@lib/util.js`:
- Around line 1456-1472: In _loadParser, ensure createRequire is called with an
absolute path by wrapping dir with _toAbsolutePath (i.e.
createRequire(_toAbsolutePath(dir))) to avoid issues when NODE_CONFIG_DIR is
relative, and modify the catch block so it does not implicitly return undefined
— on failure return defaultParser (or DEFAULT_OPTIONS.parser where appropriate)
and still log the error (keep the console.warn/console.log or processLogger) so
downstream code (e.g. DEFAULT_OPTIONS.parser consumers) is not clobbered by an
undefined value.

In `@parser.js`:
- Around line 6-7: The second createRequire call is still given a directory
(process.cwd()) while the first uses Path.join(process.cwd(), 'package.json');
update the require initializer to use an anchor file like the first one for
consistency: replace createRequire(process.cwd()) with
createRequire(Path.join(process.cwd(), 'package.json')) so both moduleRequire
and require are created from the same package.json anchor (locate the two
createRequire assignments for moduleRequire and require).

In `@test/16-config/parser/custom-2.js`:
- Around line 1-21: The file uses ESM import/export but tests expect CJS
loading; update the module to support both formats by keeping the existing
import of Parser and at the end add a CJS export fallback so the Parser object
is available to require()-based consumers (i.e., ensure module.exports = Parser
when module.exports is defined). Keep the existing Parser.setParser(...) and
Parser.setFilesOrder(...) calls unchanged so behavior remains identical.

In `@test/16-custom-parser.js`:
- Around line 26-31: Tests use mixed conventions for NODE_CONFIG_PARSER paths
(custom-1.js vs custom-2/custom-3 without extension) which can break an ESM
import() loader; make the convention explicit by updating the test to include
the .js extension for all parser env values (change occurrences that set
NODE_CONFIG_PARSER for custom-2 and custom-3 to end with .js), and re-run tests;
if instead you prefer loader compatibility, ensure the parser loader
(_loadParser / setParserOrder / setParser in lib/util.js) resolves missing
extensions via createRequire or a resolver before calling import() so bare names
won’t fail—pick one approach and apply consistently across the test cases.

In `@test/2-config-test.js`:
- Around line 18-47: The tests are order-dependent because only the first
beforeEach sets process-wide variables (NODE_CONFIG_DIR, NODE_ENV, NODE_CONFIG,
process.argv and related process.env keys) before calling
requireUncached('./lib/config.mjs'); fix by centralizing that setup: create a
shared setup helper (or move the existing beforeEach block) that sets/clears
NODE_CONFIG_DIR, NODE_ENV, NODE_APP_INSTANCE, NODE_CONFIG, all CUSTOM_* env vars
and resets process.argv, and call it in every describe’s beforeEach (or the
top-level beforeEach) and ensure you always call
requireUncached('./lib/config.mjs') after this reset so each describe gets a
clean env/argv for config initialization.

In `@test/9-config/default.js`:
- Around line 1-16: The file currently uses CommonJS module.exports
(module.exports = ({raw}) => {...}) which can break when the package is
published as "type": "module"; update this to an ESM-friendly export or
explicitly make the file CommonJS: either (A) change the export to "export
default ({raw}) => {...}" to be ESM-compatible, or (B) rename the file to a .cjs
extension or ensure package.json has "type": "commonjs" so module.exports
remains valid; verify the change covers the factory and referenced symbols
(module.exports, raw, yell, aPromise, nestedRaw) and align the decision with the
findings in test/3-config/local.js.

In `@test/type-files/defer.ts`:
- Line 1: The import of deferConfig and DeferredConfig from
'../../types/lib/defer' cannot be resolved; confirm whether the module file
exists and either add the missing types file (exporting deferConfig and
DeferredConfig from types/lib/defer as .ts or .d.ts) or change the import in
test/type-files/defer.ts to the correct existing module path that exports those
symbols; locate references to deferConfig and DeferredConfig in the repo to
decide whether to create the types file or update the import accordingly.

---

Nitpick comments:
In `@lib/config.mjs`:
- Around line 14-15: DEFAULT_CLONE_DEPTH is duplicated in lib/config.mjs and
lib/util.js; remove the duplicate and import the canonical value instead: export
DEFAULT_CLONE_DEPTH from lib/util.js (or move it to a new shared constants
module) and replace the local const DEFAULT_CLONE_DEPTH in lib/config.mjs with
an import of that exported symbol (keep LOAD_SYMBOL unchanged).

In `@test/_utils/requireUncached.mjs`:
- Around line 8-11: The helper requireUncached currently always returns
module.default which yields undefined for modules that only export named
members; update the function requireUncached to return the default export if
present otherwise the whole module (e.g., return module.default ?? module) so
callers receive either the default or the module object, ensuring resilience
across both default and named-export modules.

In `@test/0-util.js`:
- Around line 1187-1199: The tests construct configDir using string
concatenation with import.meta.dirname (e.g., in the Load instantiation calls
around Load and load.loadCustomEnvVars), which is inconsistent with other call
sites that use Path.join; update those Load({nodeEnv: ..., configDir:
import.meta.dirname + '/config'}) usages to build the path with
Path.join(import.meta.dirname, 'config') (and similarly for any other
concatenations noted) so path separators are handled consistently across
platforms.

In `@test/11-regexp.js`:
- Around line 8-14: The test setup uses beforeEach to set env and reload config
(process.env assignments and config = await requireUncached('./lib/config.mjs'))
which causes unnecessary repeated async imports; change beforeEach to before so
the environment variables (import.meta.dirname + '/config', NODE_ENV,
NODE_APP_INSTANCE) and the single requireUncached('./lib/config.mjs') call run
once for the suite. Update the hook name from beforeEach to before in this file
(referencing beforeEach, before, config, requireUncached, import.meta.dirname)
and ensure the async function signature is preserved.

In `@test/16-custom-parser.js`:
- Around line 78-80: The after() hook currently deletes
process.env.NODE_CONFIG_PARSER unconditionally; capture the prior value at file
scope (e.g., const OLD_NODE_CONFIG_PARSER = process.env.NODE_CONFIG_PARSER)
before tests run and in the after(async function () { ... }) restore it instead
of deleting: if the captured value is undefined delete the env var, otherwise
set process.env.NODE_CONFIG_PARSER = OLD_NODE_CONFIG_PARSER. Update references
around the existing after(...) block and the top of the test file where
NODE_CONFIG_PARSER is touched.

In `@test/8-config-extending.js`:
- Around line 28-29: The assignment to process.env.NODE_CONFIG_DIR inside the
test is redundant because before() already sets it; remove the duplicate
assignment on the test lines (the second process.env.NODE_CONFIG_DIR =
import.meta.dirname + '/8-config') so the test uses the value established by
before() and avoid dead code while keeping the existing filename =
path.join(process.env.NODE_CONFIG_DIR,'base-config.json') reference intact.

In `@test/makeImmutable-shared-refs.js`:
- Around line 1-5: Replace the env-sensitive indirect access via Config.util by
importing the utility module directly: remove the const util = Config.util line
and instead import { Util } (or the specific exported makeImmutable) from
'../lib/util.js' and use Util.makeImmutable (or the imported makeImmutable) in
the tests; this replaces references to Config and the Config.util symbol with
direct references to Util or makeImmutable to avoid depending on Config's
singleton initialization.

In `@test/x-config-ts/default.ts`:
- Around line 3-44: Add explicit TypeScript types to the bootstrap signature to
avoid implicit-any/implicit-this: change function bootstrap({ defer }) to
annotate the destructured param and the return type (e.g. function bootstrap({
defer }: { defer: any }): Config) so the compiler knows defer's type and the
function returns Config; ensure the Config type reference remains and adjust to
a tighter defer signature later if desired.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
parser.js (1)

151-158: ⚠️ Potential issue | 🔴 Critical

icedParser is missing its lazy-load guard and always returns undefined.

Two bugs in this function:

  1. Line 152 (changed in this PR): The if (!Iced) guard was removed, causing moduleRequire(ICED_DEP) and Iced.register() to execute on every .iced file invocation, unlike every other lazy-loaded parser in this file.
  2. There is no return require(filename) — unlike coffeeParser, the function returns undefined for every .iced config file.
🐛 Proposed fix
 Parser.icedParser = function(filename, content) {
-  Iced = moduleRequire(ICED_DEP);
-
-  // coffee-script >= 1.7.0 requires explicit registration for require() to work
-  if (Iced.register) {
-    Iced.register();
+  if (!Iced) {
+    Iced = moduleRequire(ICED_DEP);
+    // coffee-script >= 1.7.0 requires explicit registration for require() to work
+    if (Iced.register) {
+      Iced.register();
+    }
   }
+  return require(filename);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@parser.js` around lines 151 - 158, Parser.icedParser currently lacks the
lazy-load guard and a return, causing moduleRequire(ICED_DEP) and
Iced.register() to run every time and the function to return undefined; add the
same lazy-load pattern used by other parsers: check if (!Iced) then set Iced =
moduleRequire(ICED_DEP) and call Iced.register() if available, and finally
return require(filename) so Parser.icedParser returns the parsed module. Ensure
you reference the existing symbols Iced, moduleRequire, ICED_DEP, Iced.register,
and require(filename) when making the change.
🧹 Nitpick comments (2)
lib/config.mjs (1)

15-15: DEFAULT_CLONE_DEPTH duplicates the constant already defined in lib/util.js.

Both files define 20 independently. Exporting it from lib/util.js and importing it here would prevent a future silent divergence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/config.mjs` at line 15, Replace the duplicated numeric literal by
importing the shared constant from lib/util.js: remove the local declaration of
DEFAULT_CLONE_DEPTH in lib/config.mjs and add an import for the exported
DEFAULT_CLONE_DEPTH from lib/util.js (or export it from lib/util.js first if it
isn’t exported), then use that imported DEFAULT_CLONE_DEPTH wherever the local
constant was used.
parser.js (1)

21-32: Prefer const for module-level constants that are never reassigned.

All dependency name strings (lines 21–32) and the Parser object (line 38) are declared with let but never reassigned.

♻️ Proposed change
-let COFFEE_2_DEP = 'coffeescript',
+const COFFEE_2_DEP = 'coffeescript',
     COFFEE_DEP = 'coffee-script',
     ...
     TS_DEP = 'ts-node';

-let Parser = {};
+const Parser = {};

Also applies to: 38-38

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@parser.js` around lines 21 - 32, The module-level dependency name variables
(COFFEE_2_DEP, COFFEE_DEP, ICED_DEP, JS_YAML_DEP, YAML_DEP, JSON5_DEP,
HJSON_DEP, TOML_DEP, CSON_DEP, PPARSER_DEP, XML_DEP, TS_DEP) and the Parser
object are declared with let but never reassigned; change their declarations to
const to reflect immutability—update the multi-variable declaration starting
with COFFEE_2_DEP to use const and change the declaration of Parser to const as
well (preserving the same identifiers and values).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/config.mjs`:
- Around line 680-699: The code mutates envConfig by calling
Util.extendDeep(envConfig, cmdLineConfig, {}), causing the object already pushed
into additional to be modified and the CLI settings applied twice; fix by
merging into a new object instead of mutating envConfig — call
Util.extendDeep({}, envConfig, cmdLineConfig) (remove the redundant trailing
{}), use that result in load.setEnv('NODE_CONFIG', ...) so additional still
holds the original envConfig reference, and then call load.scan(additional) as
before.

In `@parser.js`:
- Line 91: The conditional uses a misleading and incomplete optional chain;
change the check to fully guard the extensions property by replacing "if
(require?.extensions['.ts'] === undefined)" with a safe chained access like "if
(require.extensions?.['.ts'] === undefined)" so that access to ['.ts'] is
protected if extensions is absent (remove the unnecessary optional on require
since it is non-nullable).

---

Outside diff comments:
In `@parser.js`:
- Around line 151-158: Parser.icedParser currently lacks the lazy-load guard and
a return, causing moduleRequire(ICED_DEP) and Iced.register() to run every time
and the function to return undefined; add the same lazy-load pattern used by
other parsers: check if (!Iced) then set Iced = moduleRequire(ICED_DEP) and call
Iced.register() if available, and finally return require(filename) so
Parser.icedParser returns the parsed module. Ensure you reference the existing
symbols Iced, moduleRequire, ICED_DEP, Iced.register, and require(filename) when
making the change.

---

Duplicate comments:
In `@lib/util.js`:
- Around line 1468-1471: The catch block in _loadParser currently just logs the
error and lets the function return undefined, which then overwrites
DEFAULT_OPTIONS.parser in the Load constructor and causes
options.parser.getFilesOrder() to throw; update the catch inside _loadParser to
return the provided defaultParser (and optionally include the error in the
warning message) so callers always receive a valid parser instance and
DEFAULT_OPTIONS.parser cannot be replaced with undefined.

---

Nitpick comments:
In `@lib/config.mjs`:
- Line 15: Replace the duplicated numeric literal by importing the shared
constant from lib/util.js: remove the local declaration of DEFAULT_CLONE_DEPTH
in lib/config.mjs and add an import for the exported DEFAULT_CLONE_DEPTH from
lib/util.js (or export it from lib/util.js first if it isn’t exported), then use
that imported DEFAULT_CLONE_DEPTH wherever the local constant was used.

In `@parser.js`:
- Around line 21-32: The module-level dependency name variables (COFFEE_2_DEP,
COFFEE_DEP, ICED_DEP, JS_YAML_DEP, YAML_DEP, JSON5_DEP, HJSON_DEP, TOML_DEP,
CSON_DEP, PPARSER_DEP, XML_DEP, TS_DEP) and the Parser object are declared with
let but never reassigned; change their declarations to const to reflect
immutability—update the multi-variable declaration starting with COFFEE_2_DEP to
use const and change the declaration of Parser to const as well (preserving the
same identifiers and values).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
parser.js (1)

151-158: ⚠️ Potential issue | 🟠 Major

icedParser does not return the parsed configuration.

When parsing .iced files, the function registers iced-coffee-script but fails to return the result of require(filename), causing it to silently return undefined. Unlike coffeeParser (lines 122-145), which explicitly returns the parsed config, icedParser only performs setup without completing the parse operation. Additionally, it lacks the cache guard used by coffeeParser, causing the module to reload on every invocation.

Add the missing return require(filename) statement and implement module caching:

Proposed fix
 Parser.icedParser = function(filename, content) {
-  Iced = moduleRequire(ICED_DEP);
+  if (!Iced) {
+    Iced = moduleRequire(ICED_DEP);
+  }
 
   // coffee-script >= 1.7.0 requires explicit registration for require() to work
   if (Iced.register) {
     Iced.register();
   }
+  return require(filename);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@parser.js` around lines 151 - 158, Parser.icedParser currently only registers
iced-coffee-script and never returns the parsed module or uses caching; modify
Parser.icedParser to mirror coffeeParser by checking require.cache for filename
and returning the cached module.exports if present, otherwise require(filename)
and return its exports (i.e., add a cache guard and a final "return
require(filename)" equivalent) so the parsed configuration is returned and
modules aren't reloaded on every call.
♻️ Duplicate comments (1)
lib/config.mjs (1)

697-697: ⚠️ Potential issue | 🟠 Major

Util.extendDeep(envConfig, cmdLineConfig, {}) mutates envConfig already held by reference in additional.

When both NODE_CONFIG env var and --NODE_CONFIG CLI arg are set:

  1. envConfig reference is pushed into additional at line 680
  2. Line 697 mutates envConfig in-place to include cmdLineConfig
  3. additional[0].config now contains the merged result
  4. load.scan(additional) then applies (envConfig ∪ cmdLine) and cmdLineConfig separately — CLI values are applied twice

Additionally, the trailing {} argument is unnecessary and misleading.

🐛 Proposed fix
-  load.setEnv('NODE_CONFIG', JSON.stringify(Util.extendDeep(envConfig, cmdLineConfig, {})));
+  load.setEnv('NODE_CONFIG', JSON.stringify(Util.extendDeep({}, envConfig, cmdLineConfig)));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/config.mjs` at line 697, The current call to Util.extendDeep(envConfig,
cmdLineConfig, {}) mutates envConfig (which was earlier pushed into additional),
causing CLI values to be applied twice; change the call to merge into a fresh
object so envConfig is not mutated (use Util.extendDeep({}, envConfig,
cmdLineConfig)) and remove the trailing empty object argument so load.setEnv
receives a new merged object without altering additional[0].config.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@parser.js`:
- Around line 151-158: Parser.icedParser currently only registers
iced-coffee-script and never returns the parsed module or uses caching; modify
Parser.icedParser to mirror coffeeParser by checking require.cache for filename
and returning the cached module.exports if present, otherwise require(filename)
and return its exports (i.e., add a cache guard and a final "return
require(filename)" equivalent) so the parsed configuration is returned and
modules aren't reloaded on every call.

---

Duplicate comments:
In `@lib/config.mjs`:
- Line 697: The current call to Util.extendDeep(envConfig, cmdLineConfig, {})
mutates envConfig (which was earlier pushed into additional), causing CLI values
to be applied twice; change the call to merge into a fresh object so envConfig
is not mutated (use Util.extendDeep({}, envConfig, cmdLineConfig)) and remove
the trailing empty object argument so load.setEnv receives a new merged object
without altering additional[0].config.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab50588 and 37fe1cf.

📒 Files selected for processing (3)
  • lib/config.mjs
  • lib/util.js
  • parser.js

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
parser.js (1)

151-158: ⚠️ Potential issue | 🔴 Critical

Parser.icedParser drops parsed output and can break config loading.

At Line 152-158, the function only registers iced-coffee-script and returns undefined. That can propagate an undefined config object into merge/load paths.

🔧 Proposed fix
 Parser.icedParser = function(filename, content) {
-  Iced = moduleRequire(ICED_DEP);
-
-  // coffee-script >= 1.7.0 requires explicit registration for require() to work
-  if (Iced.register) {
-    Iced.register();
-  }
+  if (!Iced) {
+    Iced = moduleRequire(ICED_DEP);
+    // coffee-script >= 1.7.0 requires explicit registration for require() to work
+    if (Iced.register) {
+      Iced.register();
+    }
+  }
+  return require(filename);
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@parser.js` around lines 151 - 158, Parser.icedParser currently only registers
the iced-coffee-script module and returns undefined; modify Parser.icedParser to
actually parse/compile the provided content and return the result (so callers
get a valid config object instead of undefined). Specifically, after requiring
Iced and calling Iced.register(), invoke the appropriate Iced
parsing/compilation API (e.g., Iced.compile or Iced.parse depending on the
installed iced-coffee-script version) with the content and filename and return
that value from Parser.icedParser so downstream merge/load paths receive the
parsed output.
test/1-protected-test.js (1)

27-45: ⚠️ Potential issue | 🟡 Minor

process.argv.push modification persists across tests.

Line 39 pushes --NODE_CONFIG=... to process.argv, but this is never removed. Since beforeEach runs before every test, this will keep appending the same argument, and the modifications will persist to subsequent test files.

🛡️ Proposed fix - save and restore argv
   let config;
+  let originalArgv;

   beforeEach(async function () {
+    originalArgv = process.argv.slice();
     // Change the configuration directory for testing
     process.env.NODE_CONFIG_DIR = import.meta.dirname + '/config';
     // ... existing setup ...
     process.argv.push('--NODE_CONFIG={"EnvOverride":{"parm5":"overridden from --NODE_CONFIG","parm6":101}}');

     config = await requireUncached('./lib/config.mjs');
   });

+  afterEach(function() {
+    process.argv = originalArgv;
+  });

Note: Import afterEach from node:test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/1-protected-test.js` around lines 27 - 45, The test setup pushes a
persistent arg into process.argv inside beforeEach (the beforeEach block that
calls requireUncached('./lib/config.mjs')), causing argv pollution across tests;
fix by saving the original process.argv at the start of beforeEach (or module
top) and restore it in an afterEach hook (import afterEach from 'node:test') so
the push in beforeEach is undone after each test, ensuring process.argv is
returned to its original value before calling requireUncached.
♻️ Duplicate comments (7)
lib/config.mjs (1)

696-697: ⚠️ Potential issue | 🟠 Major

Util.extendDeep(envConfig, cmdLineConfig, {}) still mutates envConfig held in additional.

This was flagged in a previous review. When both NODE_CONFIG env var and --NODE_CONFIG CLI arg are set:

  1. Line 680 pushes { config: envConfig } into additional
  2. Line 697 mutates envConfig in-place, so additional[0].config now contains the merged result
  3. load.scan(additional) applies the CLI values twice

The trailing {} argument is also unnecessary.

🐛 Proposed fix
-  load.setEnv('NODE_CONFIG', JSON.stringify(Util.extendDeep(envConfig, cmdLineConfig, {})));
+  load.setEnv('NODE_CONFIG', JSON.stringify(Util.extendDeep({}, envConfig, cmdLineConfig)));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/config.mjs` around lines 696 - 697, The current call
Util.extendDeep(envConfig, cmdLineConfig, {}) mutates envConfig (which is stored
in additional[0].config) causing CLI values to be applied twice; change the
merge to start from a fresh target so envConfig is not modified (e.g., call
Util.extendDeep({}, envConfig, cmdLineConfig)) and remove the unnecessary
trailing {} argument, then pass the resulting stringified object into
load.setEnv('NODE_CONFIG', ...); this preserves additional's original envConfig
and prevents double application when load.scan(additional) runs.
lib/config.js (1)

1-4: ⚠️ Potential issue | 🔴 Critical

require('./config.mjs') is incompatible with the supported Node.js versions and will throw ERR_REQUIRE_ESM on Node.js 20.x.

Synchronous require() of ES modules (.mjs files) is only supported unflagged starting in Node.js 22.12.0. On Node.js 20.x (which is within the supported engine range >= 20.11.0), this code path will throw immediately, blocking all CommonJS consumers.

This CommonJS bridge file should use createRequire from node:module instead, matching the pattern used in parser.js and lib/util.js.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/config.js` around lines 1 - 4, Replace the direct synchronous require of
an .mjs ES module with Node's createRequire so CommonJS consumers on Node 20.x
won't hit ERR_REQUIRE_ESM: import createRequire from 'node:module' (use the same
pattern as parser.js/lib/util.js), build a require function via
createRequire(import.meta.url) or equivalent used in those files, then load the
ESM with that require and set module.exports = requiredModule.default while
keeping the existing /** `@typedef` */ and /** `@type` */ comments intact.
test/22-binary.js (1)

10-16: ⚠️ Potential issue | 🟡 Minor

Environment variables set in beforeEach are not cleaned up.

NODE_CONFIG_DIR, NODE_ENV, and NODE_APP_INSTANCE are set but never restored. They persist for any test files that execute after this one, potentially interfering with tests that don't explicitly configure those variables.

🛡️ Proposed fix - add afterEach cleanup
+  let originalConfigDir, originalNodeEnv, originalAppInstance;
+
+  afterEach(function() {
+    if (originalConfigDir === undefined) delete process.env.NODE_CONFIG_DIR;
+    else process.env.NODE_CONFIG_DIR = originalConfigDir;
+    if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
+    else process.env.NODE_ENV = originalNodeEnv;
+    if (originalAppInstance === undefined) delete process.env.NODE_APP_INSTANCE;
+    else process.env.NODE_APP_INSTANCE = originalAppInstance;
+  });
+
   beforeEach(async function() {
+    originalConfigDir = process.env.NODE_CONFIG_DIR;
+    originalNodeEnv = process.env.NODE_ENV;
+    originalAppInstance = process.env.NODE_APP_INSTANCE;
     process.env.NODE_CONFIG_DIR = import.meta.dirname + '/config';

Note: Import afterEach from node:test as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/22-binary.js` around lines 10 - 16, The beforeEach sets environment
variables NODE_CONFIG_DIR, NODE_ENV, and NODE_APP_INSTANCE but never restores
them; add an afterEach (imported from node:test) that clears or restores those
env vars after each test run and also clears any cached module state used by
requireUncached (mirror the teardown behavior to avoid leaking
NODE_CONFIG_DIR/NODE_ENV/NODE_APP_INSTANCE into other tests); ensure afterEach
resets process.env.NODE_CONFIG_DIR, process.env.NODE_ENV,
process.env.NODE_APP_INSTANCE (or restores prior values) and re-invokes
requireUncached cleanup if necessary.
test/x-deferred-configs-ts.js (1)

20-20: ⚠️ Potential issue | 🟡 Minor

Formatting issue: closing brace on same line as await.

The }); that closes the before callback is on the same line as the await statement. This appears to be a merge artifact that wasn't fully resolved.

🔧 Proposed fix
-    CONFIG = await requireUncached('./lib/config.mjs');  });
+    CONFIG = await requireUncached('./lib/config.mjs');
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/x-deferred-configs-ts.js` at line 20, The closing brace and parenthesis
for the before callback are on the same line as the await call making the
formatting wrong; edit the test where CONFIG is assigned via
requireUncached('./lib/config.mjs') so that the await expression ends with its
semicolon on one line and the closing sequence `});` is moved to its own
following line (i.e., separate the await call from the `});` that closes the
before callback), locating the code around the CONFIG assignment and the before
block (symbols: CONFIG, requireUncached, before).
test/2-config-test.js (1)

65-67: ⚠️ Potential issue | 🟡 Minor

beforeEach in sibling describe blocks doesn't reset environment variables.

In node:test, beforeEach is scoped to its own describe block. The "Immutability" block (and several other sibling blocks) only calls requireUncached without resetting NODE_CONFIG_DIR, NODE_ENV, NODE_CONFIG, etc.

If tests run in isolation (e.g., --test-name-pattern), environment variables will be whatever the process inherited, potentially causing config load failures. Consider either promoting the env-var setup to a shared before at the top-level describe, or repeating the env setup in each sibling block.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/2-config-test.js` around lines 65 - 67, Test environment variables are
not being reset for sibling describe blocks because beforeEach in each block
only calls requireUncached('./lib/config.mjs') without restoring
NODE_CONFIG_DIR, NODE_ENV, NODE_CONFIG, etc.; fix by moving the env setup/reset
into a shared hook or adding env-reset in each sibling beforeEach: update the
top-level describe (or each describe containing beforeEach) to set/clear
process.env.NODE_CONFIG_DIR, process.env.NODE_ENV, process.env.NODE_CONFIG to
known values before requiring via requireUncached, and ensure any previous
values are restored/cleared after each test to avoid cross-test pollution when
running isolated tests; look for beforeEach and requireUncached usages in
test/2-config-test.js and adjust accordingly.
test/util.js (1)

43-44: ⚠️ Potential issue | 🔴 Critical

loadFileConfigs called with incorrect argument type.

Line 43 passes a string directly: util.loadFileConfigs(path.join(import.meta.dirname, '5-config')). The function expects either a Load instance or a LoadOptions object. Per the JSDoc and implementation, the correct form is: util.loadFileConfigs({configDir: path.join(import.meta.dirname, '5-config')}) as used in test/0-util.js line 1394. Passing a string will result in options.configDir being undefined, causing the function to fail.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/util.js` around lines 43 - 44, The test calls loadFileConfigs with a raw
string which doesn't match the expected signature; update the call to pass a
LoadOptions object instead—use util.loadFileConfigs({ configDir:
path.join(import.meta.dirname, '5-config') }) so that loadFileConfigs receives
options.configDir correctly (refer to the loadFileConfigs function and existing
usage in test/0-util.js for the expected shape).
test/16-custom-parser.js (1)

9-13: ⚠️ Potential issue | 🟠 Major

Force-clear NODE_CONFIG_PARSER in the default-parser sanity setup.

Line 9-13 loads config without resetting NODE_CONFIG_PARSER, so external shell state can make this test non-deterministic.

Suggested fix
     beforeEach(async function() {
       process.env.NODE_CONFIG_DIR = import.meta.dirname + '/16-config';
+      delete process.env.NODE_CONFIG_PARSER;
 
       config = await requireUncached('./lib/config.mjs');
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/16-custom-parser.js` around lines 9 - 13, The test's beforeEach setup
doesn't clear NODE_CONFIG_PARSER so external shell state can affect
requireUncached('./lib/config.mjs'); modify the beforeEach (the function
containing process.env.NODE_CONFIG_DIR and the call to requireUncached) to
explicitly unset or clear process.env.NODE_CONFIG_PARSER (e.g., delete
process.env.NODE_CONFIG_PARSER or set it to an empty string) before calling
requireUncached so the default parser is used deterministically.
🧹 Nitpick comments (1)
test/8-config-extending.js (1)

26-37: Redundant NODE_CONFIG_DIR assignment inside the test.

Line 28 sets NODE_CONFIG_DIR again, but it was already set in the before hook at line 13 with the same value. This is unnecessary duplication.

♻️ Proposed fix
   describe('Extending a base configuration with another configuration', function() {
     it('Extending a configuration with another configuration should work without error', function () {
-      process.env.NODE_CONFIG_DIR = import.meta.dirname + '/8-config';
       let filename = path.join(process.env.NODE_CONFIG_DIR,'base-config.json');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/8-config-extending.js` around lines 26 - 37, Remove the redundant
environment reset inside the test: delete the `process.env.NODE_CONFIG_DIR =
import.meta.dirname + '/8-config';` line from the `it` block since the `before`
hook already sets `NODE_CONFIG_DIR`; leave the rest of the test (variables
`filename`, `base_config`, `CONFIG.util.attachProtoDeep`, and the
`Util.extendDeep` assertion) unchanged so behavior remains identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@parser.js`:
- Around line 151-158: Parser.icedParser currently only registers the
iced-coffee-script module and returns undefined; modify Parser.icedParser to
actually parse/compile the provided content and return the result (so callers
get a valid config object instead of undefined). Specifically, after requiring
Iced and calling Iced.register(), invoke the appropriate Iced
parsing/compilation API (e.g., Iced.compile or Iced.parse depending on the
installed iced-coffee-script version) with the content and filename and return
that value from Parser.icedParser so downstream merge/load paths receive the
parsed output.

In `@test/1-protected-test.js`:
- Around line 27-45: The test setup pushes a persistent arg into process.argv
inside beforeEach (the beforeEach block that calls
requireUncached('./lib/config.mjs')), causing argv pollution across tests; fix
by saving the original process.argv at the start of beforeEach (or module top)
and restore it in an afterEach hook (import afterEach from 'node:test') so the
push in beforeEach is undone after each test, ensuring process.argv is returned
to its original value before calling requireUncached.

---

Duplicate comments:
In `@lib/config.js`:
- Around line 1-4: Replace the direct synchronous require of an .mjs ES module
with Node's createRequire so CommonJS consumers on Node 20.x won't hit
ERR_REQUIRE_ESM: import createRequire from 'node:module' (use the same pattern
as parser.js/lib/util.js), build a require function via
createRequire(import.meta.url) or equivalent used in those files, then load the
ESM with that require and set module.exports = requiredModule.default while
keeping the existing /** `@typedef` */ and /** `@type` */ comments intact.

In `@lib/config.mjs`:
- Around line 696-697: The current call Util.extendDeep(envConfig,
cmdLineConfig, {}) mutates envConfig (which is stored in additional[0].config)
causing CLI values to be applied twice; change the merge to start from a fresh
target so envConfig is not modified (e.g., call Util.extendDeep({}, envConfig,
cmdLineConfig)) and remove the unnecessary trailing {} argument, then pass the
resulting stringified object into load.setEnv('NODE_CONFIG', ...); this
preserves additional's original envConfig and prevents double application when
load.scan(additional) runs.

In `@test/16-custom-parser.js`:
- Around line 9-13: The test's beforeEach setup doesn't clear NODE_CONFIG_PARSER
so external shell state can affect requireUncached('./lib/config.mjs'); modify
the beforeEach (the function containing process.env.NODE_CONFIG_DIR and the call
to requireUncached) to explicitly unset or clear process.env.NODE_CONFIG_PARSER
(e.g., delete process.env.NODE_CONFIG_PARSER or set it to an empty string)
before calling requireUncached so the default parser is used deterministically.

In `@test/2-config-test.js`:
- Around line 65-67: Test environment variables are not being reset for sibling
describe blocks because beforeEach in each block only calls
requireUncached('./lib/config.mjs') without restoring NODE_CONFIG_DIR, NODE_ENV,
NODE_CONFIG, etc.; fix by moving the env setup/reset into a shared hook or
adding env-reset in each sibling beforeEach: update the top-level describe (or
each describe containing beforeEach) to set/clear process.env.NODE_CONFIG_DIR,
process.env.NODE_ENV, process.env.NODE_CONFIG to known values before requiring
via requireUncached, and ensure any previous values are restored/cleared after
each test to avoid cross-test pollution when running isolated tests; look for
beforeEach and requireUncached usages in test/2-config-test.js and adjust
accordingly.

In `@test/22-binary.js`:
- Around line 10-16: The beforeEach sets environment variables NODE_CONFIG_DIR,
NODE_ENV, and NODE_APP_INSTANCE but never restores them; add an afterEach
(imported from node:test) that clears or restores those env vars after each test
run and also clears any cached module state used by requireUncached (mirror the
teardown behavior to avoid leaking NODE_CONFIG_DIR/NODE_ENV/NODE_APP_INSTANCE
into other tests); ensure afterEach resets process.env.NODE_CONFIG_DIR,
process.env.NODE_ENV, process.env.NODE_APP_INSTANCE (or restores prior values)
and re-invokes requireUncached cleanup if necessary.

In `@test/util.js`:
- Around line 43-44: The test calls loadFileConfigs with a raw string which
doesn't match the expected signature; update the call to pass a LoadOptions
object instead—use util.loadFileConfigs({ configDir:
path.join(import.meta.dirname, '5-config') }) so that loadFileConfigs receives
options.configDir correctly (refer to the loadFileConfigs function and existing
usage in test/0-util.js for the expected shape).

In `@test/x-deferred-configs-ts.js`:
- Line 20: The closing brace and parenthesis for the before callback are on the
same line as the await call making the formatting wrong; edit the test where
CONFIG is assigned via requireUncached('./lib/config.mjs') so that the await
expression ends with its semicolon on one line and the closing sequence `});` is
moved to its own following line (i.e., separate the await call from the `});`
that closes the before callback), locating the code around the CONFIG assignment
and the before block (symbols: CONFIG, requireUncached, before).

---

Nitpick comments:
In `@test/8-config-extending.js`:
- Around line 26-37: Remove the redundant environment reset inside the test:
delete the `process.env.NODE_CONFIG_DIR = import.meta.dirname + '/8-config';`
line from the `it` block since the `before` hook already sets `NODE_CONFIG_DIR`;
leave the rest of the test (variables `filename`, `base_config`,
`CONFIG.util.attachProtoDeep`, and the `Util.extendDeep` assertion) unchanged so
behavior remains identical.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37fe1cf and 9692d24.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (45)
  • async.js
  • defer.js
  • lib/config.js
  • lib/config.mjs
  • lib/defer.js
  • lib/util.js
  • package.json
  • parser.js
  • raw.js
  • test/0-util.js
  • test/1-protected-test.js
  • test/11-regexp.js
  • test/15-async-configs.js
  • test/15-config/default.js
  • test/15-config/local.js
  • test/16-config/parser/custom-1.js
  • test/16-config/parser/custom-2.js
  • test/16-config/parser/custom-3.js
  • test/16-custom-parser.js
  • test/2-config-test.js
  • test/21-date.js
  • test/21-reentrant/default.js
  • test/21-reentrant/nested/default.js
  • test/22-binary.js
  • test/23-serialize.js
  • test/3-config/local.js
  • test/3-deferred-configs.js
  • test/5-getConfigSources.js
  • test/6-strict-mode.js
  • test/8-config-extending.js
  • test/9-config/default.js
  • test/9-raw-configs.js
  • test/_utils/requireUncached.js
  • test/_utils/requireUncached.mjs
  • test/config/default-array-merge.js
  • test/makeImmutable-shared-refs.js
  • test/type-files/async.ts
  • test/type-files/defer.ts
  • test/type-files/parser.ts
  • test/type-files/raw.ts
  • test/type-files/util.ts
  • test/util.js
  • test/x-config-ts/default.ts
  • test/x-config-ts/local.ts
  • test/x-deferred-configs-ts.js
💤 Files with no reviewable changes (12)
  • test/_utils/requireUncached.js
  • test/21-reentrant/nested/default.js
  • raw.js
  • test/21-reentrant/default.js
  • test/15-config/local.js
  • test/15-config/default.js
  • test/15-async-configs.js
  • test/config/default-array-merge.js
  • defer.js
  • async.js
  • test/type-files/async.ts
  • test/type-files/raw.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • test/type-files/defer.ts
  • test/9-config/default.js
  • test/type-files/parser.ts
  • test/16-config/parser/custom-1.js
  • test/_utils/requireUncached.mjs
  • test/23-serialize.js
  • package.json
  • test/makeImmutable-shared-refs.js

@jdmarshall jdmarshall merged commit 062cdd3 into node-config:master Mar 1, 2026
4 checks passed
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.

1 participant