ReScript Namespaces and Module Shadowing
How the flat module namespace works
ReScript (like OCaml) compiles every .res file to a module interface (.cmi) and places it in a flat global namespace. All modules visible to the compiler — from the current package and its dependencies — coexist in this namespace. Without any scoping mechanism, a CommandTopic.res from one package would collide with a CommandTopic.res from another.
The "namespace" setting
The "namespace" key in rescript.json is the scoping mechanism:
| Setting | Effect |
|---|---|
"namespace": "Reventless" | Each file Foo.res is internally renamed to Reventless__Foo.cmi. A wrapper Reventless.cmi re-exports them as submodules (module Foo = Reventless__Foo). The bare name Foo is not in the global namespace — only Reventless.Foo. |
"namespace": false (or omitted) | Files keep their plain names (Foo.cmi). They land directly in the flat global namespace of every consuming package, accessible unqualified as Foo. |
Why some modules are available unqualified
There are two sources of unqualified module access in a ReScript package:
1. Same-package modules
All .res files within a single package share a flat namespace with each other. If reventless-local lists both src/ and tests/ in its "sources", every file in both directories sees every other file unqualified — regardless of subdirectory. This is why TestRunner, LocalBus, AsyncTest, DcbEventLog_Builder etc. are all accessible without a prefix inside the test files.
2. Dependencies with "namespace": false
Dependency packages that omit a namespace expose their modules globally. The key example in this codebase:
// @reventlessdev/rescript-jest/rescript.json
{
"namespace": false
}
Because @reventlessdev/rescript-jest declares no namespace, its JestGlobals
module lands directly in the global flat namespace of every package that depends
on it — which is why open JestGlobals works with no prefix in test files.
Similarly, sury (the schema library) likely has no namespace, which is why S is accessible unqualified.
By contrast, reventless-spec ("namespace": "Reventless") and reventless ("namespace": "Reventless") namespace all their modules — so you must write Reventless.CommandTopic and ReventlessCoreCommandTopic, not bare CommandTopic.
Warning 44: open statement shadows an identifier
ReScript warning 44 fires when an open statement introduces a name that already exists in scope:
Warning 44: this open statement shadows the module identifier CommandTopic
This warning appeared in DcbE2EFixtures.res because the file contained:
open Reventless // ← warning 44: shadows CommandTopic
Before the open, CommandTopic referred to ReventlessCoreCommandTopic (the implementation module, which has getHandlers). After the open, CommandTopic was re-bound to Reventless.CommandTopic (the type-only specification, which does NOT have getHandlers). Any code that called CommandTopic.getHandlers would then fail at the type-check stage.
Fix
Remove the broad open and qualify all uses explicitly:
// Before (problematic):
open Reventless
...
let handlers = CommandTopic.getHandlers(typeName) // resolves to Reventless.CommandTopic — no getHandlers!
// After (correct):
let handlers = ReventlessCoreCommandTopic.getHandlers(typeName)
...
let testMeta: Reventless.Message.meta = { ... }
General advice
- Prefer
openat a small scope (inside aletblock or function) rather than at module top level. This limits the shadowing risk. - Avoid
opening two packages that share module names (e.g.,open Reventlessand then relying onReventlessCoreCommandTopicunqualified). - Warning 44 is a signal, not noise — it usually means a wrong module will be selected after the open. Treat it as an error (
+44inrescript.json"warnings": { "error": "+44" }). - Use
-44only when the shadowing is intentional and you have verified correctness. Thereventless-localpackage uses"-44"to suppress it as a non-error, relying on compiler warnings being visible in the build output.