Boxwerk is an experimental Ruby package system with Box-powered constant isolation. It is used at runtime to organize code into packages with explicit dependencies and strict constant access using Ruby::Box. Inspired by Packwerk.
- Strict Isolation: Each package runs in its own
Ruby::Box, preventing constant leakage - Explicit Dependencies: Dependencies declared in
package.yml, validated as a DAG - Controlled Exports: Only declared constants are accessible to importers
- Flexible Imports: Multiple strategies (namespaced, aliased, selective, renamed)
- Lazy Loading: Exports loaded on-demand when imported
- No gem isolationβall gems are global across packages
- No constant reloading support
- Exported constants must follow Zeitwerk naming conventions
- Console runs in root box, not root package box (due to IRB loading issues)
Ruby::Boxitself is experimental in Ruby 4.0
- Ruby 4.0+ with
RUBY_BOX=1environment variable set.
my_app/
βββ Gemfile
βββ package.yml # Root package manifest
βββ app.rb # Your application entrypoint
βββ packages/
βββ finance/
βββ package.yml # Package manifest
βββ lib/
βββ invoice.rb # Defines Invoice
βββ tax_calculator.rb # Defines TaxCalculator
Gemfile:
source 'https://rubygems.org'
gem 'boxwerk'
gem 'money' # Example: gems are auto-required and globally accessibleRoot package.yml:
imports:
- packages/finance # Will define a `Finance` module to hold finance package exportspackages/finance/package.yml:
exports:
- Invoice
- TaxCalculatorapp.rb:
# No requires needed - imports are wired by Boxwerk
invoice = Finance::Invoice.new(10_000)
puts invoice.total # => #<Money fractional:10000 currency:USD>RUBY_BOX=1 boxwerk run app.rbBoxwerk handles Bundler setup, gem loading, package wiring, and script execution automatically.
See the example/ directory for a working multi-package application:
cd example
RUBY_BOX=1 boxwerk run app.rbRun a script:
boxwerk run script.rb [args...]Interactive console (currently runs in root box, not root package):
boxwerk console [irb-args...]Help:
boxwerk helpA package.yml defines what a package exports and imports:
exports:
- PublicClass
- PublicModule
imports:
- packages/dependency1
- packages/dependency2: AliasConstants that should be visible to packages that import this one. Exports are lazily loaded during boot; only those actually imported by dependent packages are loaded.
Package dependencies that are wired as new constants in the importing package's box. Default and aliased namespace imports create a module to hold the exports. Not transitive: if A imports B and B imports C, A cannot access C without explicitly importing it.
Default namespace (all exports under package name):
imports:
- packages/finance
# Result: Finance::Invoice, Finance::TaxCalculatorAliased namespace (custom module name):
imports:
- packages/finance: Billing
# Result: Billing::Invoice, Billing::TaxCalculatorNote: Single exports import directly without namespace
Selective import (specific constants):
imports:
- packages/finance:
- Invoice
- TaxCalculator
# Result: Invoice, TaxCalculator (no namespace)Selective rename (custom names):
imports:
- packages/finance:
Invoice: Bill
TaxCalculator: Calculator
# Result: Bill, CalculatorAll gems in your Gemfile are:
- Automatically loaded in the root box via Bundler
- Accessible globally in all packages (no gem isolation)
- No manual
requireorpackage.ymldeclaration needed
Related to Ruby::Box in Ruby 4.0+. See Ruby::Box documentation for details.
- Gem requiring: Crashes VM when requiring gems inside boxes after boot (workaround: gems pre-loaded in root box)
- Console context: Runs in root box instead of root package box due to IRB loading limitation
- IRB autocomplete: Disabled by since it currently crashes VMpe
Boot process:
- Setup Bundler and require all gems in root box
- Find root
package.ymlby searching up from current directory - Build and validate dependency graph (DAG)
- Boot packages in topological order, creating isolated boxes
- Wire imports by lazily loading exports and injecting constants
- Execute command in root package context
Components:
- CLI: Parses commands, validates environment, delegates to Setup
- Setup: Finds root package, builds graph, creates registry, boots packages
- Graph: Builds DAG, validates no cycles, performs topological sort
- Package: Parses
package.yml, tracks exports/imports/box - Loader: Creates boxes, loads exports lazily (Zeitwerk conventions), wires imports
- Registry: Tracks booted packages, ensures single boot per package
After checking out the repo, run bin/setup to install dependencies. Then, run the tests:
RUBY_BOX=1 bundle exec rake testTo install this gem onto your local machine, run bundle exec rake install.
The gem is available as open source under the terms of the MIT License.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you shall be dual licensed as above, without any additional terms or conditions.