carton 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/CHANGELOG.md +92 -1
- data/README.md +103 -27
- data/lib/carton/activate_gems.rb +8 -0
- data/lib/carton/box.rb +162 -68
- data/lib/carton/bundler.rb +73 -0
- data/lib/carton/error.rb +24 -0
- data/lib/carton/gem_activation.rb +151 -0
- data/lib/carton/import_cache.rb +105 -0
- data/lib/carton/kernel_patch.rb +12 -8
- data/lib/carton/loaded_features.rb +10 -0
- data/lib/carton/namespace.rb +78 -0
- data/lib/carton/{export_methods.rb → namespace_methods.rb} +14 -3
- data/lib/carton/resolver.rb +149 -0
- data/lib/carton/runtime.rb +74 -59
- data/lib/carton/version.rb +1 -1
- data/lib/carton.rb +13 -22
- data/sig/carton.rbs +49 -0
- metadata +20 -34
- data/AGENTS.md +0 -47
- data/DESIGN.md +0 -84
- data/Rakefile +0 -52
- data/TODO.md +0 -33
- data/USAGE.md +0 -173
- data/examples/complex/README.md +0 -33
- data/examples/complex/cartons/adventure/.env +0 -1
- data/examples/complex/cartons/adventure/Gemfile +0 -5
- data/examples/complex/cartons/adventure/Gemfile.lock +0 -17
- data/examples/complex/cartons/adventure/lib/adventure.rb +0 -12
- data/examples/complex/cartons/loot/.env +0 -1
- data/examples/complex/cartons/loot/Gemfile +0 -5
- data/examples/complex/cartons/loot/Gemfile.lock +0 -17
- data/examples/complex/cartons/loot/lib/loot.rb +0 -47
- data/examples/complex/cartons/quest/lib/quest.rb +0 -6
- data/examples/complex/main.rb +0 -37
- data/examples/minimal/README.md +0 -16
- data/examples/minimal/bar.rb +0 -10
- data/examples/minimal/baz.rb +0 -7
- data/examples/minimal/foo.rb +0 -7
- data/examples/minimal/main.rb +0 -17
- data/images/carton.png +0 -0
- data/lib/carton/exports.rb +0 -35
data/USAGE.md
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
# Usage
|
|
2
|
-
|
|
3
|
-
## Requirements
|
|
4
|
-
|
|
5
|
-
- Ruby 4.0+
|
|
6
|
-
- `RUBY_BOX=1`
|
|
7
|
-
- `require 'carton'` before using `import`, `import_relative`, `export_default`, or `export`
|
|
8
|
-
|
|
9
|
-
In Carton, an importable package is called a carton. This guide still says "package" sometimes in plain English, but "carton" is the project term.
|
|
10
|
-
|
|
11
|
-
## Exporting
|
|
12
|
-
|
|
13
|
-
### Single export
|
|
14
|
-
|
|
15
|
-
Use a single export when the file represents one main thing.
|
|
16
|
-
|
|
17
|
-
```ruby
|
|
18
|
-
# user.rb
|
|
19
|
-
class User
|
|
20
|
-
def initialize(name) = @name = name
|
|
21
|
-
|
|
22
|
-
def greet = "Hello, #{@name}!"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
export_default User
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
Importing that file returns the exported object itself:
|
|
29
|
-
|
|
30
|
-
```ruby
|
|
31
|
-
User = import_relative 'user'
|
|
32
|
-
User.new('Alice').greet
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### Named exports
|
|
36
|
-
|
|
37
|
-
Use keyword arguments when the file exports a small namespace.
|
|
38
|
-
|
|
39
|
-
```ruby
|
|
40
|
-
# math.rb
|
|
41
|
-
def add(a, b) = a + b
|
|
42
|
-
|
|
43
|
-
export(
|
|
44
|
-
PI: 3.14159,
|
|
45
|
-
version: '1.0.0',
|
|
46
|
-
add: method(:add)
|
|
47
|
-
)
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Importing named exports returns a `Carton::Exports` object, which behaves like a namespace module.
|
|
51
|
-
|
|
52
|
-
```ruby
|
|
53
|
-
MathTools = import_relative 'math'
|
|
54
|
-
|
|
55
|
-
MathTools::PI
|
|
56
|
-
MathTools.version
|
|
57
|
-
MathTools.add(2, 3)
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`Carton::Exports` and bare `Carton::Box` imports share a small lookup API: `[]`, `fetch`, `fetch_values`, `values_at`, and `key?`.
|
|
61
|
-
|
|
62
|
-
Only one export call is allowed per imported file. Use `export_default value` for single exports or `export foo:, bar:` for named exports.
|
|
63
|
-
|
|
64
|
-
## Importing
|
|
65
|
-
|
|
66
|
-
### `import_relative`
|
|
67
|
-
|
|
68
|
-
`import_relative path` resolves relative to the calling file, like `require_relative`.
|
|
69
|
-
|
|
70
|
-
```ruby
|
|
71
|
-
User = import_relative 'user'
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### `import`
|
|
75
|
-
|
|
76
|
-
`import path` resolves using the current box's `$LOAD_PATH` or an absolute path.
|
|
77
|
-
|
|
78
|
-
```ruby
|
|
79
|
-
Quest = import 'quest'
|
|
80
|
-
Widget = import '/absolute/path/to/widget.rb'
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
When you want `import 'name'` to work across local cartons, add their `lib/` directories to `$LOAD_PATH` first.
|
|
84
|
-
|
|
85
|
-
```ruby
|
|
86
|
-
cartons_dir = File.expand_path('cartons', __dir__)
|
|
87
|
-
|
|
88
|
-
Dir.glob(File.join(cartons_dir, '*/lib')).sort.each do |dir|
|
|
89
|
-
$LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir)
|
|
90
|
-
end
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
The complex example keeps that setup explicit in `examples/complex/main.rb`.
|
|
94
|
-
|
|
95
|
-
## Destructuring
|
|
96
|
-
|
|
97
|
-
Named exports support pattern matching through `deconstruct_keys`.
|
|
98
|
-
|
|
99
|
-
```ruby
|
|
100
|
-
import_relative('math') => { add:, version: }
|
|
101
|
-
|
|
102
|
-
add.(2, 3)
|
|
103
|
-
version
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
You can rename during destructuring:
|
|
107
|
-
|
|
108
|
-
```ruby
|
|
109
|
-
import_relative('math') => { add: sum }
|
|
110
|
-
sum.(10, 10)
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Constants
|
|
114
|
-
|
|
115
|
-
For exported constants, the simplest options are namespace access, `[]`, or `fetch`.
|
|
116
|
-
|
|
117
|
-
```ruby
|
|
118
|
-
MathTools = import_relative 'math'
|
|
119
|
-
PI = MathTools::PI
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
```ruby
|
|
123
|
-
PI = import('math')[:PI]
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
For multiple values:
|
|
127
|
-
|
|
128
|
-
```ruby
|
|
129
|
-
PI, version = import_relative('math').fetch_values(:PI, :version)
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
## Bare imports
|
|
133
|
-
|
|
134
|
-
If a file does not call `export`, importing it returns the `Carton::Box` itself.
|
|
135
|
-
|
|
136
|
-
```ruby
|
|
137
|
-
Toolbox = import 'some_script'
|
|
138
|
-
|
|
139
|
-
Toolbox::SomeConstant
|
|
140
|
-
Toolbox.fetch(:helper)
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## Bundler inside cartons
|
|
144
|
-
|
|
145
|
-
The library works with or without Bundler. Plain cartons need no extra setup. For a bundled carton, the reliable pattern today is still: select the Gemfile around the `import` that loads that carton.
|
|
146
|
-
|
|
147
|
-
```ruby
|
|
148
|
-
gemfile = File.expand_path('cartons/adventure/Gemfile', __dir__)
|
|
149
|
-
|
|
150
|
-
Adventure = Carton.with_bundle(gemfile) { import 'adventure' }
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
`Carton.with_bundle` is just a small wrapper around the same `BUNDLE_GEMFILE` handoff. The underlying constraint is still Bundler's: `bundler/setup` discovers the active Gemfile from env/process state, and RubyGems activation state (`Gem.loaded_specs`) is still shared across boxes in practice, so conflicting bundles still cannot be switched reliably in one process today.
|
|
154
|
-
|
|
155
|
-
Current limits:
|
|
156
|
-
|
|
157
|
-
- one carton-local bundle can be activated cleanly from an unbundled parent
|
|
158
|
-
- a bundled parent process is not a reliable place to import a child carton with its own bundle
|
|
159
|
-
- conflicting bundles in one process still need a subprocess workaround
|
|
160
|
-
|
|
161
|
-
See [TODO.md](TODO.md) for the upstream Bundler work that would make this cleaner.
|
|
162
|
-
|
|
163
|
-
## Examples
|
|
164
|
-
|
|
165
|
-
```sh
|
|
166
|
-
RUBY_BOX=1 ruby examples/minimal/main.rb
|
|
167
|
-
RUBY_BOX=1 bundle exec rake example:complex
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
See the example READMEs for details:
|
|
171
|
-
|
|
172
|
-
- [examples/minimal/README.md](examples/minimal/README.md)
|
|
173
|
-
- [examples/complex/README.md](examples/complex/README.md)
|
data/examples/complex/README.md
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# Complex example
|
|
2
|
-
|
|
3
|
-
Multi-carton example showing the library directly: explicit load-path setup, a bundled single export loaded by name, a plain carton loaded by name, and a destructured import.
|
|
4
|
-
|
|
5
|
-
## Cartons
|
|
6
|
-
|
|
7
|
-
| Carton | Purpose |
|
|
8
|
-
| --- | --- |
|
|
9
|
-
| `main` | explicit feature tour of the library |
|
|
10
|
-
| `adventure` | bundled carton imported by name |
|
|
11
|
-
| `quest` | plain carton imported by name |
|
|
12
|
-
| `loot` | carton that demonstrates a conflicting `dotenv` version via subprocess |
|
|
13
|
-
|
|
14
|
-
`main.rb` keeps the load-path setup and bundled import explicit on purpose so the example reads like a direct Carton feature tour. `Carton.with_bundle` shortens the `BUNDLE_GEMFILE` handoff, but it is still the same Bundler constraint under the hood.
|
|
15
|
-
|
|
16
|
-
## Why `loot` still uses a subprocess
|
|
17
|
-
|
|
18
|
-
One carton-local bundle can be activated from an unbundled parent today. The trouble starts when another box tries to activate a conflicting version: box-local `$LOAD_PATH` is not enough because RubyGems activation state is still shared. `loot` therefore loads its `dotenv` data in a subprocess and selects its Gemfile there instead of using `Carton.with_bundle` during the import itself. That is tracked in [../../TODO.md](../../TODO.md).
|
|
19
|
-
|
|
20
|
-
## Run
|
|
21
|
-
|
|
22
|
-
```sh
|
|
23
|
-
RUBY_BOX=1 bundle exec rake example:complex
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Or manually:
|
|
27
|
-
|
|
28
|
-
```sh
|
|
29
|
-
cd examples/complex/cartons/adventure && BUNDLE_GEMFILE=Gemfile bundle install
|
|
30
|
-
cd ../loot && BUNDLE_GEMFILE=Gemfile bundle install
|
|
31
|
-
cd ../../..
|
|
32
|
-
RUBY_BOX=1 ruby examples/complex/main.rb
|
|
33
|
-
```
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
BANNER=single export loaded from a bundled carton
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
GEM
|
|
2
|
-
remote: https://rubygems.org/
|
|
3
|
-
specs:
|
|
4
|
-
dotenv (3.2.0)
|
|
5
|
-
|
|
6
|
-
PLATFORMS
|
|
7
|
-
arm64-darwin-25
|
|
8
|
-
ruby
|
|
9
|
-
|
|
10
|
-
DEPENDENCIES
|
|
11
|
-
dotenv (~> 3.0)
|
|
12
|
-
|
|
13
|
-
CHECKSUMS
|
|
14
|
-
dotenv (3.2.0) sha256=e375b83121ea7ca4ce20f214740076129ab8514cd81378161f11c03853fe619d
|
|
15
|
-
|
|
16
|
-
BUNDLED WITH
|
|
17
|
-
4.0.3
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'dotenv'
|
|
4
|
-
|
|
5
|
-
module Adventure
|
|
6
|
-
CONFIG = Dotenv.parse(File.expand_path('../.env', __dir__))
|
|
7
|
-
def self.banner = CONFIG.fetch('BANNER')
|
|
8
|
-
|
|
9
|
-
def self.dotenv_version = Gem.loaded_specs.fetch('dotenv').version.to_s
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
export_default Adventure
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
MESSAGE=loaded through a conflicting bundle in a subprocess
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
GEM
|
|
2
|
-
remote: https://rubygems.org/
|
|
3
|
-
specs:
|
|
4
|
-
dotenv (2.8.1)
|
|
5
|
-
|
|
6
|
-
PLATFORMS
|
|
7
|
-
arm64-darwin-25
|
|
8
|
-
ruby
|
|
9
|
-
|
|
10
|
-
DEPENDENCIES
|
|
11
|
-
dotenv (~> 2.0)
|
|
12
|
-
|
|
13
|
-
CHECKSUMS
|
|
14
|
-
dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8
|
|
15
|
-
|
|
16
|
-
BUNDLED WITH
|
|
17
|
-
4.0.3
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'open3'
|
|
5
|
-
require 'rbconfig'
|
|
6
|
-
|
|
7
|
-
Plans = import 'quest'
|
|
8
|
-
|
|
9
|
-
# This carton is imported plainly by name. The subprocess below is where it
|
|
10
|
-
# selects its own Gemfile, because doing the conflicting dotenv activation
|
|
11
|
-
# directly in another box still collides through shared RubyGems activation
|
|
12
|
-
# state and can even crash under Ruby::Box today.
|
|
13
|
-
dotenv_payload =
|
|
14
|
-
begin
|
|
15
|
-
env_file = File.expand_path('../.env', __dir__)
|
|
16
|
-
gemfile = File.expand_path('../Gemfile', __dir__)
|
|
17
|
-
script = <<~'RUBY'
|
|
18
|
-
require 'json'
|
|
19
|
-
require 'bundler/setup'
|
|
20
|
-
require 'dotenv'
|
|
21
|
-
|
|
22
|
-
config = Dotenv.parse(ARGV.fetch(0))
|
|
23
|
-
version = Gem.loaded_specs.fetch('dotenv').version.to_s
|
|
24
|
-
puts JSON.generate(version:, config:)
|
|
25
|
-
RUBY
|
|
26
|
-
|
|
27
|
-
output, status =
|
|
28
|
-
Open3.capture2e(
|
|
29
|
-
{ 'BUNDLE_GEMFILE' => gemfile },
|
|
30
|
-
RbConfig.ruby,
|
|
31
|
-
'-e',
|
|
32
|
-
script,
|
|
33
|
-
env_file,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
unless status.success?
|
|
37
|
-
raise RuntimeError, "failed to load loot dotenv bundle: #{output}"
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
JSON.parse output
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
dotenv_version = dotenv_payload.fetch 'version'
|
|
44
|
-
legacy_message =
|
|
45
|
-
"#{dotenv_payload.fetch('config').fetch('MESSAGE')} | quest says: #{Plans.fetch :summary}"
|
|
46
|
-
|
|
47
|
-
export legacy_message:, DOTENV_VERSION: dotenv_version
|
data/examples/complex/main.rb
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative '../../lib/carton'
|
|
4
|
-
|
|
5
|
-
cartons_dir = File.expand_path('cartons', __dir__)
|
|
6
|
-
|
|
7
|
-
# Local carton lib directories still need to be on the caller's load path when
|
|
8
|
-
# we want import 'name' to resolve inside nested boxes.
|
|
9
|
-
Dir
|
|
10
|
-
.glob(File.join(cartons_dir, '*/lib'))
|
|
11
|
-
.sort
|
|
12
|
-
.each { |dir| $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir) }
|
|
13
|
-
|
|
14
|
-
adventure_gemfile = File.expand_path('cartons/adventure/Gemfile', __dir__)
|
|
15
|
-
|
|
16
|
-
# Bundler still picks the active Gemfile from env/process state today, so the
|
|
17
|
-
# bundled import stays explicit even though the wrapper is small.
|
|
18
|
-
Adventure = Carton.with_bundle(adventure_gemfile) { import 'adventure' }
|
|
19
|
-
|
|
20
|
-
Plans = import 'quest'
|
|
21
|
-
|
|
22
|
-
# loot still imports by name, but it does not need Carton.with_bundle here
|
|
23
|
-
# because the conflicting dotenv activation happens entirely in the subprocess
|
|
24
|
-
# inside loot.rb, not during this import itself.
|
|
25
|
-
import('loot') => { legacy_message:, DOTENV_VERSION: loot_dotenv_version }
|
|
26
|
-
|
|
27
|
-
puts '-- Bundled single export --'
|
|
28
|
-
puts "Adventure.banner = #{Adventure.banner}"
|
|
29
|
-
puts "Adventure.dotenv_version = #{Adventure.dotenv_version}"
|
|
30
|
-
puts
|
|
31
|
-
puts '-- Named export loaded by name --'
|
|
32
|
-
puts "Plans::FEATURES = #{Plans::FEATURES.join(', ')}"
|
|
33
|
-
puts "Plans.summary = #{Plans.fetch :summary}"
|
|
34
|
-
puts
|
|
35
|
-
puts '-- Destructured named export --'
|
|
36
|
-
puts "loot dotenv version = #{loot_dotenv_version}"
|
|
37
|
-
puts "legacy_message = #{legacy_message}"
|
data/examples/minimal/README.md
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# Minimal example
|
|
2
|
-
|
|
3
|
-
Smallest working example of the library.
|
|
4
|
-
|
|
5
|
-
## What it shows
|
|
6
|
-
|
|
7
|
-
- single export (`foo.rb`, `baz.rb`)
|
|
8
|
-
- named exports (`bar.rb`)
|
|
9
|
-
- `import_relative`
|
|
10
|
-
- one carton importing another carton internally
|
|
11
|
-
|
|
12
|
-
## Run
|
|
13
|
-
|
|
14
|
-
```sh
|
|
15
|
-
RUBY_BOX=1 ruby examples/minimal/main.rb
|
|
16
|
-
```
|
data/examples/minimal/bar.rb
DELETED
data/examples/minimal/baz.rb
DELETED
data/examples/minimal/foo.rb
DELETED
data/examples/minimal/main.rb
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative '../../lib/carton'
|
|
4
|
-
|
|
5
|
-
# Single import — Foo exports a module
|
|
6
|
-
Foo = import_relative 'foo'
|
|
7
|
-
|
|
8
|
-
# Namespace import — Bar exports a hash with methods and constants
|
|
9
|
-
Bar = import_relative 'bar'
|
|
10
|
-
|
|
11
|
-
puts '-- Foo --'
|
|
12
|
-
puts Foo.hello
|
|
13
|
-
|
|
14
|
-
puts
|
|
15
|
-
puts '-- Bar (uses Baz internally) --'
|
|
16
|
-
puts Bar.hello
|
|
17
|
-
puts "Bar::MAGIC = #{Bar::MAGIC}"
|
data/images/carton.png
DELETED
|
Binary file
|
data/lib/carton/exports.rb
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Carton
|
|
4
|
-
class Exports < ::Module
|
|
5
|
-
include ExportMethods
|
|
6
|
-
|
|
7
|
-
def initialize(values = {})
|
|
8
|
-
super()
|
|
9
|
-
@values = values
|
|
10
|
-
@lookup = {}
|
|
11
|
-
define_exports
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
private
|
|
15
|
-
|
|
16
|
-
def define_exports
|
|
17
|
-
@values.each do |key, value|
|
|
18
|
-
@lookup[key.to_s] = value
|
|
19
|
-
|
|
20
|
-
if key.to_s.match?(/\A[A-Z]/)
|
|
21
|
-
const_set(key, value)
|
|
22
|
-
else
|
|
23
|
-
define_singleton_method(
|
|
24
|
-
key,
|
|
25
|
-
) { |*args, **kwargs, &block| value.respond_to?(:call) ? value.call(*args, **kwargs, &block) : value }
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def lookup_entry(key)
|
|
31
|
-
name = key.to_s
|
|
32
|
-
@lookup.key?(name) ? [true, @lookup[name]] : [false, nil]
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|