boxwerk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +24 -0
- data/ARCHITECTURE.md +264 -0
- data/CHANGELOG.md +74 -3
- data/README.md +61 -335
- data/Rakefile +46 -3
- data/TODO.md +317 -0
- data/USAGE.md +505 -0
- data/exe/boxwerk +55 -22
- data/lib/boxwerk/autoloader_mixin.rb +65 -0
- data/lib/boxwerk/box_manager.rb +405 -0
- data/lib/boxwerk/cli.rb +775 -68
- data/lib/boxwerk/constant_resolver.rb +236 -0
- data/lib/boxwerk/gem_resolver.rb +235 -0
- data/lib/boxwerk/gemfile_require_parser.rb +50 -0
- data/lib/boxwerk/global_context.rb +85 -0
- data/lib/boxwerk/package.rb +71 -28
- data/lib/boxwerk/package_context.rb +103 -0
- data/lib/boxwerk/package_resolver.rb +122 -0
- data/lib/boxwerk/privacy_checker.rb +159 -0
- data/lib/boxwerk/setup.rb +123 -36
- data/lib/boxwerk/version.rb +1 -1
- data/lib/boxwerk/zeitwerk_scanner.rb +172 -0
- data/lib/boxwerk.rb +26 -4
- metadata +54 -24
- data/example/Gemfile +0 -6
- data/example/Gemfile.lock +0 -66
- data/example/README.md +0 -130
- data/example/app.rb +0 -101
- data/example/package.yml +0 -6
- data/example/packages/finance/lib/invoice.rb +0 -51
- data/example/packages/finance/lib/tax_calculator.rb +0 -26
- data/example/packages/finance/package.yml +0 -10
- data/example/packages/util/lib/calculator.rb +0 -21
- data/example/packages/util/lib/geometry.rb +0 -26
- data/example/packages/util/package.yml +0 -5
- data/lib/boxwerk/graph.rb +0 -111
- data/lib/boxwerk/loader.rb +0 -277
- data/lib/boxwerk/registry.rb +0 -37
- data/sig/boxwerk.rbs +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78eb85866eb8a42cb597f88faeace9f97fb81c8312748767669f776fbb09f504
|
|
4
|
+
data.tar.gz: 78b358fa881a3c0e440d52c67e479c70d29b4d6a4ae79a9eebaa0872e5e92605
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae1b0a336dfbaa4f01fccb376a38b2c7020fccae033eeabcad328f20c2fdb312c8bbc14a37eea771b518c180125d6f8224cb3516e4d837e5484dc60a6eb307de
|
|
7
|
+
data.tar.gz: 1b31b4d1a59d2213617b522f3ab88f88a2a3c7ef686ce817fb7bb9e56a2680c47bfa597f7dae7385dbddc35c1a2594911c5a18aa067910604525b98df1a79c10
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Agents
|
|
2
|
+
|
|
3
|
+
Guidelines for AI agents working on this codebase.
|
|
4
|
+
|
|
5
|
+
- Write tests for new behaviour; run existing tests before committing.
|
|
6
|
+
- Only run tests relevant to your changes — not the full suite every time.
|
|
7
|
+
- If you only changed an example, only run that example's tests.
|
|
8
|
+
- Run targeted tests as you go for fast feedback; run the full suite once before committing.
|
|
9
|
+
- Whenever adding new features/functionality, update complex example with support for it (if appropriate). Also add to or update E2E tests.
|
|
10
|
+
- Update relevant documentation (README, USAGE, ARCHITECTURE, TODO, example READMEs, CHANGELOG).
|
|
11
|
+
- Keep README concise — detailed usage belongs in USAGE.md.
|
|
12
|
+
- Keep documentation minimal — don't be wordy.
|
|
13
|
+
- Consistent style with existing code and docs.
|
|
14
|
+
- Commit as you go with descriptive messages.
|
|
15
|
+
- Don't over-engineer — keep it simple.
|
|
16
|
+
- When multiple implementations or design choices exist, present a menu to the user with your recommendation.
|
|
17
|
+
- Run `RUBY_BOX=1 bundle exec rake` to run all tests (unit, e2e, examples).
|
|
18
|
+
- Run `RUBY_BOX=1 bundle exec rake test` for unit tests only.
|
|
19
|
+
- Run `RUBY_BOX=1 bundle exec rake test e2e` for unit and e2e tests.
|
|
20
|
+
- Run `RUBY_BOX=1 bundle exec rake example:minimal` for a specific example.
|
|
21
|
+
- Run `bundle exec rake format` to format code after every change.
|
|
22
|
+
- Fix any warnings.
|
|
23
|
+
- Use sub-agents where appropriate.
|
|
24
|
+
- Never bump version or publish gem.
|
data/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
This document describes how Boxwerk works internally.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Boxwerk enforces package boundaries at runtime using Ruby::Box isolation. Each package gets its own `Ruby::Box` instance. Constants are resolved lazily on first access and cached. Only direct dependencies are accessible; transitive dependencies are blocked.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌─────────────────────────────────────────────────────┐
|
|
11
|
+
│ Root Box │
|
|
12
|
+
│ (Bundler + global gems loaded here) │
|
|
13
|
+
│ │
|
|
14
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
|
15
|
+
│ │ root pkg │ │ finance │ │ util │ ... │
|
|
16
|
+
│ │ (box) │ │ (box) │ │ (box) │ │
|
|
17
|
+
│ └──────────┘ └──────────┘ └──────────┘ │
|
|
18
|
+
│ │ │ │ │
|
|
19
|
+
│ │ const_missing autoload │
|
|
20
|
+
│ │ searches deps resolves files │
|
|
21
|
+
└─────────────────────────────────────────────────────┘
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Ruby::Box Primer
|
|
25
|
+
|
|
26
|
+
[`Ruby::Box`](https://docs.ruby-lang.org/en/4.0/Ruby/Box.html) provides in-process isolation of classes, modules, and constants. Boxwerk relies on these specific behaviours:
|
|
27
|
+
|
|
28
|
+
### Box Types
|
|
29
|
+
|
|
30
|
+
- **Root box** — A single box per Ruby process. Created during bootstrap. All builtin classes/modules live here. The source for copy-on-write when creating user boxes.
|
|
31
|
+
- **Main box** — A user box automatically created at bootstrap, copied from the root box. The user's main program runs here.
|
|
32
|
+
- **User boxes** — Created with `Ruby::Box.new`, copied from the root box. All user boxes are flat (no nesting). This is what Boxwerk creates for each package.
|
|
33
|
+
|
|
34
|
+
### Key Behaviours
|
|
35
|
+
|
|
36
|
+
- **File scope.** One `.rb` file runs in a single box. Methods and procs defined in that file always execute in that file's box, even when called from another box.
|
|
37
|
+
- **Top-level constants.** Constants defined at the top level are constants of `Object` within that box. From outside, `box::Foo` accesses them.
|
|
38
|
+
- **Monkey patch isolation.** Reopened built-in classes are visible only within the box that defined them.
|
|
39
|
+
- **Global variable isolation.** `$LOAD_PATH`, `$LOADED_FEATURES`, and other globals are isolated per box. `$LOAD_PATH` and `$LOADED_FEATURES` use the *loading box* (not current box) for `require` resolution.
|
|
40
|
+
- **Copy-on-write.** `Ruby::Box.new` copies the root box's class extensions. Anything loaded into the root box *before* creating a user box is inherited. Anything loaded *after* is not.
|
|
41
|
+
- **`box.eval(code)`** — Evaluates Ruby code in the box's context, like loading a file.
|
|
42
|
+
- **`box.require(path)`** — Requires a file in the box's context. Subsequent requires from that file also run in the same box.
|
|
43
|
+
|
|
44
|
+
### Important Implications
|
|
45
|
+
|
|
46
|
+
1. **Order matters.** Gems loaded into the root box via `Bundler.require` before `Ruby::Box.new` are inherited by all user boxes. This is how Boxwerk provides "global gems".
|
|
47
|
+
2. **No cross-box method inheritance.** A `const_missing` hook defined on `Module` in one box does not fire in another box. Boxwerk must install resolvers per-box.
|
|
48
|
+
3. **`$LOAD_PATH` per box.** Each box has its own `$LOAD_PATH`, which enables per-package gem version isolation.
|
|
49
|
+
|
|
50
|
+
## Boot Sequence
|
|
51
|
+
|
|
52
|
+
The `boxwerk` executable (`exe/boxwerk`) orchestrates the boot:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
1. exe/boxwerk starts in the main box
|
|
56
|
+
2. If Bundler is loaded (bundle exec or binstub), re-exec into a clean
|
|
57
|
+
Ruby process using Bundler.with_unbundled_env (prevents double gem loading)
|
|
58
|
+
3. For install/info/help/version: load boxwerk directly (no Ruby::Box needed)
|
|
59
|
+
4. For exec/run/console: check Ruby::Box availability and enabled status
|
|
60
|
+
5. Switch to root box via Ruby::Box.root.eval(...)
|
|
61
|
+
6. Load boxwerk gem into root box ($LOAD_PATH.unshift)
|
|
62
|
+
7. Discover project Gemfile (gems.rb preferred, then Gemfile)
|
|
63
|
+
8. Run Bundler.setup + Bundler.require in root box
|
|
64
|
+
→ All global gems now available in root box
|
|
65
|
+
9. Call Boxwerk::CLI.run(ARGV)
|
|
66
|
+
→ CLI delegates to Setup.run for package boot
|
|
67
|
+
→ Discovers boxwerk.yml for configuration (package_paths)
|
|
68
|
+
→ Runs global boot (global/ require + global/boot.rb)
|
|
69
|
+
→ Eager-loads Zeitwerk constants for child box inheritance
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Setup.run
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
1. Find root package.yml or boxwerk.yml (walk up from current directory)
|
|
76
|
+
2. Run global boot (if global/ exists):
|
|
77
|
+
a. Require global/ files in root box (eager, not autoload)
|
|
78
|
+
b. Require global/boot.rb in root box
|
|
79
|
+
3. Eager-load all Zeitwerk-managed constants in root box
|
|
80
|
+
4. Create PackageResolver — discovers all package.yml files
|
|
81
|
+
→ Respects package_paths from boxwerk.yml
|
|
82
|
+
→ Creates implicit root package if no root package.yml exists
|
|
83
|
+
5. Create BoxManager — manages Ruby::Box instances
|
|
84
|
+
6. Boot all packages in topological order (dependencies first)
|
|
85
|
+
→ Root package boot.rb runs in root package box (not root box)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### BoxManager.boot (per package)
|
|
89
|
+
|
|
90
|
+
For each package, in dependency order:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
1. Create Ruby::Box.new (copied from root box, inherits global gems)
|
|
94
|
+
2. Setup per-package gem load paths (if Gemfile exists)
|
|
95
|
+
→ Prepend gem load paths to the box's $LOAD_PATH
|
|
96
|
+
3. Auto-require gems from Gemfile (non-root packages only)
|
|
97
|
+
→ Respects require: false and require: 'custom/path'
|
|
98
|
+
4. Scan directories with Zeitwerk (file discovery + inflection)
|
|
99
|
+
→ Uses Zeitwerk::Loader::FileSystem for scanning
|
|
100
|
+
→ Uses Zeitwerk::Inflector for snake_case → CamelCase conversion
|
|
101
|
+
5. Register autoload entries in the box via box.eval
|
|
102
|
+
→ Implicit namespaces: create Module.new
|
|
103
|
+
→ Explicit namespaces: autoload + eager trigger
|
|
104
|
+
→ Files: autoload :Invoice, "/path/to/public/invoice.rb"
|
|
105
|
+
6. Run per-package boot.rb (if it exists)
|
|
106
|
+
→ Executes after the package's own constants are scanned
|
|
107
|
+
→ Used for configuring additional autoload dirs and collapse dirs
|
|
108
|
+
7. Wire dependency constants via const_missing
|
|
109
|
+
→ Install a resolver that searches direct dependency boxes
|
|
110
|
+
→ When enforce_dependencies is false, searches ALL packages:
|
|
111
|
+
explicit deps first (in declared order), then remaining packages
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Constant Resolution
|
|
115
|
+
|
|
116
|
+
Constants are resolved through two mechanisms:
|
|
117
|
+
|
|
118
|
+
### Intra-Package: autoload
|
|
119
|
+
|
|
120
|
+
Each package's own constants are registered as `autoload` entries in its box. When code inside the box references `Calculator`, Ruby's built-in `autoload` loads the file and defines the constant — standard Ruby behaviour.
|
|
121
|
+
|
|
122
|
+
### Cross-Package: const_missing
|
|
123
|
+
|
|
124
|
+
When a constant is not found in the current box (no autoload entry), `Object.const_missing` fires. Boxwerk installs a custom handler per-box that:
|
|
125
|
+
|
|
126
|
+
1. Iterates through the package's declared direct dependencies
|
|
127
|
+
2. For each dependency, checks if it has the constant (via file index or `const_get`)
|
|
128
|
+
3. Enforces privacy rules (public path, private_constants list)
|
|
129
|
+
4. Returns the constant value from the dependency's box
|
|
130
|
+
5. Raises `NameError` if no dependency has the constant
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# Simplified const_missing flow:
|
|
134
|
+
class Object
|
|
135
|
+
def self.const_missing(const_name)
|
|
136
|
+
deps.each do |dep|
|
|
137
|
+
next unless dep.has_constant?(const_name)
|
|
138
|
+
check_privacy!(const_name, dep)
|
|
139
|
+
return dep.box.const_get(const_name)
|
|
140
|
+
end
|
|
141
|
+
raise NameError, "uninitialized constant #{const_name}"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Constants are **not** wrapped in namespaces. `Invoice` is accessible as `Invoice`, not `Finance::Invoice`. This matches how constants work within a single Ruby application.
|
|
147
|
+
|
|
148
|
+
### Root Box Resolver (for exec/run)
|
|
149
|
+
|
|
150
|
+
When running commands via `boxwerk exec` or `boxwerk run`, some tools (like Rake) load files via the root box rather than the package box. Boxwerk installs a composite resolver on `Ruby::Box.root` that:
|
|
151
|
+
|
|
152
|
+
1. Tries the target package's own box first (for internal constants)
|
|
153
|
+
2. Falls through to the target box's dependency resolver
|
|
154
|
+
3. This ensures package constants are accessible even when code is loaded by tools running in the root box
|
|
155
|
+
|
|
156
|
+
## Package Resolution
|
|
157
|
+
|
|
158
|
+
`PackageResolver` discovers packages by scanning for `package.yml` files:
|
|
159
|
+
|
|
160
|
+
1. Start from the project root
|
|
161
|
+
2. Glob for `package.yml` files using `package_paths` from `boxwerk.yml` (default: `**/`)
|
|
162
|
+
3. Parse each YAML file into a `Package` object
|
|
163
|
+
4. If no root `package.yml` exists, create an implicit root package with `enforce_dependencies: false`, `enforce_privacy: false`, and automatic dependencies on all discovered packages
|
|
164
|
+
5. Resolve dependency order via DFS — circular dependencies are allowed (back-edges are skipped)
|
|
165
|
+
6. Provide topological ordering for boot (dependencies before dependents)
|
|
166
|
+
|
|
167
|
+
### package.yml Format
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
enforce_dependencies: true
|
|
171
|
+
dependencies:
|
|
172
|
+
- packs/util
|
|
173
|
+
- packs/billing
|
|
174
|
+
enforce_privacy: true
|
|
175
|
+
public_path: public/ # default
|
|
176
|
+
private_constants:
|
|
177
|
+
- "::InternalHelper"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
This is the standard [Packwerk](https://github.com/Shopify/packwerk) format.
|
|
181
|
+
|
|
182
|
+
## Privacy Enforcement
|
|
183
|
+
|
|
184
|
+
`PrivacyChecker` enforces which constants a package exposes:
|
|
185
|
+
|
|
186
|
+
- **public_path** (default: `public/`) — Files in this directory define the package's public API. Only these constants are accessible to dependents.
|
|
187
|
+
- **private_constants** — Explicitly private constants, blocked even if in the public path.
|
|
188
|
+
- **pack_public sigil** — Files outside the public path can be marked public with `# pack_public: true` in the first 5 lines.
|
|
189
|
+
|
|
190
|
+
Privacy is checked at constant resolution time in the `const_missing` handler. A `NameError` with a descriptive message is raised for violations.
|
|
191
|
+
|
|
192
|
+
## Per-Package Gem Isolation
|
|
193
|
+
|
|
194
|
+
`GemResolver` enables different packages to use different versions of the same gem:
|
|
195
|
+
|
|
196
|
+
1. Check if the package has a `gems.rb` or `Gemfile` (and corresponding lockfile)
|
|
197
|
+
2. Parse the lockfile with `Bundler::LockfileParser` to get gem specs
|
|
198
|
+
3. Parse the Gemfile with `GemfileRequireParser` to extract autorequire directives
|
|
199
|
+
4. Find the actual gem installation paths by searching all `Gem.path` directories
|
|
200
|
+
5. Collect full require paths for each gem and its runtime dependencies
|
|
201
|
+
6. Prepend these paths to the box's `$LOAD_PATH`
|
|
202
|
+
7. Auto-require gems declared in the Gemfile (non-root packages only):
|
|
203
|
+
- Default (`gem 'faker'`) → `require 'faker'`
|
|
204
|
+
- Custom (`gem 'foo', require: 'foo/bar'`) → `require 'foo/bar'`
|
|
205
|
+
- Disabled (`gem 'dotenv', require: false`) → skip
|
|
206
|
+
|
|
207
|
+
Since each box has its own `$LOAD_PATH`, `require 'faker'` in two different boxes can load different versions.
|
|
208
|
+
|
|
209
|
+
## File-to-Constant Mapping
|
|
210
|
+
|
|
211
|
+
Boxwerk uses Zeitwerk's inflector for file-to-constant name mapping:
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
lib/calculator.rb → Calculator
|
|
215
|
+
lib/tax_calculator.rb → TaxCalculator
|
|
216
|
+
public/invoice.rb → Invoice
|
|
217
|
+
lib/api/v2/client.rb → Api::V2::Client
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Zeitwerk cannot register autoloads directly inside `Ruby::Box` because autoload calls execute in the box where the code was defined (root box for Zeitwerk). Boxwerk works around this by using Zeitwerk only for file scanning and inflection, then registering autoloads via `box.eval`. For nested constants, implicit namespaces are created as `Module.new` instances, and explicit namespaces (with a matching `.rb` file) are eagerly loaded before child autoloads are registered.
|
|
221
|
+
|
|
222
|
+
## CLI Commands
|
|
223
|
+
|
|
224
|
+
The CLI (`Boxwerk::CLI`) provides:
|
|
225
|
+
|
|
226
|
+
| Command | Description |
|
|
227
|
+
|---------|-------------|
|
|
228
|
+
| `exec` | Run a command (gem binstub) in the boxed environment |
|
|
229
|
+
| `run` | Run a Ruby script in a package box |
|
|
230
|
+
| `console` | Start an IRB console in a package box |
|
|
231
|
+
| `info` | Show package structure and dependencies |
|
|
232
|
+
| `install` | Bundle install for all packages |
|
|
233
|
+
|
|
234
|
+
### console Implementation
|
|
235
|
+
|
|
236
|
+
Console always runs IRB in `Ruby::Box.root` rather than the target package box. The composite resolver installed on root provides the same constant access. This avoids a Ruby 4.0.1 GC bug where running IRB in child boxes with `const_missing` overrides triggers a double-free crash during process exit.
|
|
237
|
+
|
|
238
|
+
### exec Implementation
|
|
239
|
+
|
|
240
|
+
`exec` resolves the command to a gem binstub path, then evaluates the binstub's content in the target box using `box.eval(content)`. File content is evaluated directly (not via `load`) because `load` creates a new file scope where inherited DSL methods (e.g. Rake's `task`) may not be visible inside a `Ruby::Box`.
|
|
241
|
+
|
|
242
|
+
### --all Flag
|
|
243
|
+
|
|
244
|
+
The `--all` flag runs `exec` for each package in a separate subprocess. This is necessary because test frameworks like Minitest register tests globally via `at_exit`, which would conflict across packages in a single process.
|
|
245
|
+
|
|
246
|
+
### --global Flag
|
|
247
|
+
|
|
248
|
+
The `--global` / `-g` flag runs commands directly in `Ruby::Box.root`, bypassing package resolution entirely. No package constants are accessible — only global gems. Useful for debugging.
|
|
249
|
+
|
|
250
|
+
## Module Structure
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
Boxwerk
|
|
254
|
+
├── CLI # Command-line interface
|
|
255
|
+
├── Setup # Boot orchestration (find root, create resolver + manager)
|
|
256
|
+
├── PackageResolver # Discover packages, validate deps, topological sort
|
|
257
|
+
├── Package # Data class for a single package
|
|
258
|
+
├── BoxManager # Create boxes, scan with Zeitwerk, wire constants
|
|
259
|
+
├── ConstantResolver # Install const_missing handlers per-box
|
|
260
|
+
├── PrivacyChecker # Check public/private constant access
|
|
261
|
+
├── GemResolver # Resolve per-package gem load paths and autorequire
|
|
262
|
+
├── GemfileRequireParser # Lightweight Gemfile parser for require directives
|
|
263
|
+
└── ZeitwerkScanner # Zeitwerk-based file scanning and autoload registration
|
|
264
|
+
```
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,79 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [v0.3.0] — 2026-03-02
|
|
4
|
+
|
|
5
|
+
Complete architecture rewrite. Each package now runs in its own `Ruby::Box`
|
|
6
|
+
with constants resolved lazily at runtime via `const_missing`. Reads standard
|
|
7
|
+
Packwerk `package.yml` files without requiring Packwerk.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Package isolation** — each package runs in its own `Ruby::Box`; constants
|
|
12
|
+
from undeclared dependencies and private constants raise `NameError` at
|
|
13
|
+
runtime.
|
|
14
|
+
- **Per-package gems** — packages can declare their own `gems.rb`/`Gemfile`
|
|
15
|
+
with independent gem versions; auto-require mirrors Bundler's default
|
|
16
|
+
behaviour (respects `require: false`, `require: 'path'`).
|
|
17
|
+
- **Zeitwerk autoloading** — constants discovered via Zeitwerk conventions;
|
|
18
|
+
default autoload roots: `lib/` and `public/`.
|
|
19
|
+
- **Privacy enforcement** — `enforce_privacy`, `public_path`,
|
|
20
|
+
`private_constants`, and `# pack_public: true` file sigil.
|
|
21
|
+
- **`Boxwerk.package`** — returns a `PackageContext` in per-package `boot.rb`
|
|
22
|
+
with `name`, `root?`, `config`, `root_path`, and `autoloader`.
|
|
23
|
+
- **`Boxwerk.global`** — returns a `GlobalContext` from any box context.
|
|
24
|
+
- **Autoloader API** — `push_dir`, `collapse`, `ignore`, `setup` on both
|
|
25
|
+
`PackageContext::Autoloader` and `GlobalContext::Autoloader`; shared via
|
|
26
|
+
`AutoloaderMixin`. `push_dir` and `collapse` auto-call `setup` so constants
|
|
27
|
+
are available immediately in boot scripts.
|
|
28
|
+
- **`global/boot.rb`** — runs in the root box before package boxes; shared
|
|
29
|
+
constants defined here are inherited by all packages.
|
|
30
|
+
- **`eager_load_global`** / **`eager_load_packages`** boxwerk.yml options.
|
|
31
|
+
- **Package name normalization** — leading `./` and trailing `/` stripped;
|
|
32
|
+
`packs/foo`, `./packs/foo`, and `packs/foo/` are equivalent.
|
|
33
|
+
- **CLI commands** — `exec`, `run`, `console`, `install`, `info`.
|
|
34
|
+
- **`boxwerk install`** — installs gems for all packages; works on a fresh
|
|
35
|
+
clone without pre-installed gems.
|
|
36
|
+
- **`boxwerk info`** — shows config, global context, and per-package
|
|
37
|
+
autoload/collapse/ignore dirs (with eager-load status), enforcements,
|
|
38
|
+
dependencies, gems, and boot script presence. Boots the application
|
|
39
|
+
(`RUBY_BOX=1` required).
|
|
40
|
+
- **NameError hints** — improved error messages like
|
|
41
|
+
`(defined in 'packs/baz', not a dependency of '.')` in child package
|
|
42
|
+
contexts.
|
|
43
|
+
- **Circular dependency detection** in `boxwerk info` tree output.
|
|
44
|
+
- **`collapse` / `ignore` in boot.rb** — collapses intermediate namespaces
|
|
45
|
+
(e.g. `Analytics::Formatters` → `Analytics::*`); ignores dirs from
|
|
46
|
+
autoloading.
|
|
47
|
+
- **Missing lockfile warning** — graceful message directing to
|
|
48
|
+
`boxwerk install` when a Gemfile exists but no lockfile is found.
|
|
49
|
+
- **Monkey patch isolation** — patches defined in one package box are not
|
|
50
|
+
visible in other packages or the root context.
|
|
51
|
+
- **`examples/complex`** and **`examples/minimal`** demonstrating all features.
|
|
52
|
+
- **E2E test suite** (74 tests) alongside unit tests (120 tests).
|
|
53
|
+
- **GitHub Pages API documentation** published from `lib/` via RDoc.
|
|
54
|
+
|
|
55
|
+
### Removed
|
|
56
|
+
|
|
57
|
+
- Custom file-to-constant mapping (`Boxwerk.camelize`), replaced by Zeitwerk.
|
|
58
|
+
- Namespace wrapping.
|
|
59
|
+
|
|
60
|
+
## [v0.2.0] - 2026-01-06
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
- Simplified implementation (~370 lines removed)
|
|
64
|
+
- Consolidated cycle detection in Graph (removed redundant methods)
|
|
65
|
+
- Added class-level documentation to all modules
|
|
66
|
+
- Simplified example application
|
|
67
|
+
|
|
68
|
+
### Removed
|
|
69
|
+
- Removed `Gemfile.lock` from git (library best practice)
|
|
70
|
+
- Removed `sig/boxwerk.rbs`
|
|
71
|
+
|
|
72
|
+
## [v0.1.0] - 2026-01-05
|
|
4
73
|
|
|
5
74
|
Initial release.
|
|
6
75
|
|
|
7
|
-
[
|
|
8
|
-
[
|
|
76
|
+
[Unreleased]: https://github.com/dtcristo/boxwerk/compare/v0.3.0...HEAD
|
|
77
|
+
[v0.3.0]: https://github.com/dtcristo/boxwerk/releases/tag/v0.3.0
|
|
78
|
+
[v0.2.0]: https://github.com/dtcristo/boxwerk/releases/tag/v0.2.0
|
|
79
|
+
[v0.1.0]: https://github.com/dtcristo/boxwerk/releases/tag/v0.1.0
|