Module Resolution
Grove organizes code into modules based on the filesystem directory structure. Every directory containing
.grovefiles constitutes a module. Files within a module are merged into a single logical unit, and modules reference each other through import declarations with path-based resolution.
Prerequisites: Declaration Grammar for import syntax, Lexical Grammar for identifier rules. What you'll learn: How Grove discovers modules, merges files, resolves imports, handles test files, and determines type visibility.
Directory-Based Modules
A module is a directory that contains one or more .grove files. The module name is the directory name.
my-project/
order/
types.grove # \
events.grove # |-- these form the "order" module
actions.grove # |
order_test.grove # / (excluded from production)
customer/
types.grove # \
actions.grove # |-- these form the "customer" module
customer_test.grove # /
- The module name is derived from the directory name:
order/becomes moduleorder. - Module names should use
snake_case. - Nested directories create separate modules, not sub-modules. There is no hierarchical module system.
File Merging
All .grove files within a module directory (excluding test files) are merged into a single logical unit before analysis. This means:
- Declaration order is irrelevant. A type defined in
types.grovecan be referenced inactions.grovewithout an import. - No file-level scoping. All declarations in all files of a module share the same namespace.
- Name uniqueness. Two declarations with the same name in different files of the same module are an error.
order/
types.grove --> contains: record OrderItem { ... }
events.grove --> contains: event OrderCreated:1 { items: List<OrderItem> }
actions.grove --> contains: action create_order(...) { emit OrderCreated { ... } }
All three files are merged. OrderItem is visible in events.grove and actions.grove without any import.
Test File Exclusion
Files matching the pattern *_test.grove are excluded from production builds:
- They are included when running
grove test. - They are excluded when running
grove check,grove run, or building for deployment. - Test files may import from their own module and from other modules.
- Test files may define
testdeclarations, additional helper functions, and test fixtures.
order/
actions.grove # included in production
order_test.grove # excluded from production, included in test
helpers_test.grove # also excluded from production
The naming convention is <anything>_test.grove. The prefix does not need to match the module name.
Import Resolution
Standard Library Imports
Imports with the grove/ prefix resolve to the Grove standard library:
import "grove/time"
import "grove/http"
import "grove/json"
import "grove/crypto"
import "grove/uuid"
Standard library modules are bundled with the Grove runtime and are always available.
Local Module Imports
All other import paths are resolved relative to the importing file's directory:
// In order/actions.grove:
import "../shared/types" // resolves to <project>/shared/types/
import "../customer" // resolves to <project>/customer/
- Paths are filesystem paths using
/as the separator. ..traverses up one directory level.- The path must point to a directory that contains
.grovefiles. - Importing a specific file (e.g.,
import "../customer/types.grove") is not supported; you always import an entire module.
Import Aliasing
The as clause provides an alias for the imported module:
import "grove/time" as t
import "../shared/types" as shared
let now = t.DateTime.now()
let addr = shared.Address { street: "123 Main", city: "Springfield" }
Without an alias, the module is referenced by its directory name:
import "../shared/types"
let addr = types.Address { street: "123 Main", city: "Springfield" }
Import Cycles
Circular imports are not permitted. If module A imports module B, then B (or any module transitively imported by B) cannot import A.
Checker rule: A circular import produces: import cycle detected: <A> -> <B> -> ... -> <A>.
Type Visibility
Grove uses a Go-inspired visibility model based on qualified names:
Within a Module
All declarations within a module are visible to all files in that module without qualification. No imports are needed.
// In order/types.grove
record OrderItem {
product_id: Id
quantity: Int
price: Decimal
}
// In order/actions.grove -- no import needed
action create_order(items: List<OrderItem>) {
// OrderItem is directly visible
}
Across Modules
Declarations from other modules are accessed through qualified names using the module name (or alias) as a prefix:
import "../customer"
action create_order(customer_id: Id) {
let cust = customer.get_customer(customer_id)
// ...
}
All top-level declarations in a module are public. Grove does not have a private or internal visibility modifier. If a declaration exists in a module, it can be imported and used by other modules.
Name Conflicts
If two imported modules export the same name, you must use aliasing to disambiguate:
import "../billing/types" as billing_types
import "../shipping/types" as shipping_types
action process(
invoice: billing_types.Invoice,
label: shipping_types.ShippingLabel,
) {
// ...
}
Without aliases, two modules named types would conflict.
Module Initialization
Modules do not have initialization logic. There are no init blocks or module-level statements. All computation happens within declaration bodies (actions, functions, workflows, etc.).
Project Structure Conventions
While not enforced by the compiler, the conventional project layout is:
my-project/
grove.toml # project configuration
shared/
types/ # shared type definitions
types.grove
order/ # order domain module
types.grove
events.grove
actions.grove
queries.grove
workflows.grove
order_test.grove
customer/ # customer domain module
types.grove
events.grove
actions.grove
customer_test.grove
Each domain concept gets its own module directory. Shared types used across modules live in a shared/ area.
See Also
- Declaration Grammar -- import declaration syntax
- Semantic Constraints -- name uniqueness and cycle rules
- Configuration Reference -- project-level settings in
grove.toml - CLI Reference -- how modules are specified on the command line