carton 0.3.0 → 0.4.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 +108 -61
- data/README.md +24 -79
- data/lib/carton/box.rb +34 -60
- data/lib/carton/bundler.rb +1 -1
- data/lib/carton/error.rb +3 -4
- data/lib/carton/gem_activation.rb +13 -12
- data/lib/carton/import_cache.rb +4 -6
- data/lib/carton/kernel_patch.rb +3 -3
- data/lib/carton/resolver.rb +6 -6
- data/lib/carton/runtime.rb +20 -22
- data/lib/carton/version.rb +1 -1
- data/lib/carton.rb +0 -3
- data/sig/carton.rbs +0 -24
- metadata +6 -7
- data/lib/carton/namespace.rb +0 -78
- data/lib/carton/namespace_methods.rb +0 -58
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e0007f7095e824c2b3bc501b140288cde630695d7c56ae96c1a2f4a968c5196
|
|
4
|
+
data.tar.gz: 301c877d0c63eda3b26b18f6076097b0d8d746d7a1a0228adf7a5d13d173e9c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d3fe870cdb0bcc2ee87f9ca951abfff7c807b9847330605d25762b8be1a012952ee1c55769eefef694d2b1c52e156c4a74ef1b1640431b304ce541e5ae720b45
|
|
7
|
+
data.tar.gz: 693fcc780bf07ccbd8c85dd7e20919bb7d3cf8ab7fefbbf0f91aa990a835e0e34a3842cf8ef23c80fd9b6ad0364536be1333f5d06ae56b8d1c11b1796cef1c4b
|
data/CHANGELOG.md
CHANGED
|
@@ -2,100 +2,147 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.4.0] - 2026-10-22
|
|
6
|
+
|
|
7
|
+
Every imported Entrypoint now requires an explicit Export; Bare Cartons and `Carton::Namespace` are removed. `carton check` adds unused import and shared-object reopening warnings plus JSON reports. The Rails and Hanami examples share a Customers and Billing workflow, and Bundler support starts at 4.0.20 (#71, #79, #82, #83, #93, #100, #107).
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- Every imported Entrypoint must call `export` exactly once; an import with no Export raises `Carton::ExportError` after loading. Removed Bare Cartons and `Carton::Namespace`; adapt legacy files and gems with a distinct Entrypoint that requires them and explicitly exports the chosen value. Imports still return that value exactly, including `nil` and `false`, and plain `require` keeps working (#107).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `examples/hanami_in_cartons` implements the Rails examples' Customers/Billing workflow with Hanami slices, Dry contracts, types and operations, ROM SQLite repos, HTML/JSON responses, and a CLI bridge. Tests cover lazy/eager loading, Box ownership, persistence across processes, and Puma HTTP (#93).
|
|
16
|
+
- `carton check` warns about unused import bindings and class/module definitions that open imported shared objects, and `--format json` reports problems and counts for CI and editors. This recovers the applicable parts of the stranded static-checks work; its unused Named Export check is obsolete under the single-value Export model (#54, #82).
|
|
17
|
+
- An experimental Rails-in-Cartons reloading spike rebuilds changed domain Cartons and their Importers in a scratch copy, explicitly refreshes a retained Rails host's domain references and registered classes, and verifies requests after two reloads without losing records. It adds no runtime reload interface (#67).
|
|
18
|
+
- `examples/exports` demonstrates Hash and callable exports, an explicit adapter for legacy Ruby, and Ruby pattern matching without gems or Bundler. The plugin testing recipe is now a runnable test with its own bundle (#64, #107).
|
|
19
|
+
- `examples/dependency_injection`: a checkout Carton receives order storage, payments, and logging from its host through constructor arguments. File-backed demo adapters stay in Main, and integration tests import the Carton with fakes (#63).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- The Hanami example's Framework, Customers, Billing, and Shop are gem-shaped components under `components/`, matching the Rails example, and import each other with `import 'name'` instead of `import_relative`. Both example guides show the framework and customers Entrypoints above billing and shop (#103).
|
|
24
|
+
- Renamed the Rails and Hanami example directories to `examples/rails` and `examples/hanami`, with matching Rake tasks and updated fixture paths. Their existing docs URLs still work (#100).
|
|
25
|
+
|
|
26
|
+
- Aligned the homepage, share previews, README, docs, and gem descriptions around explicit imports and exports, separate dependencies, and runtime boundaries. The homepage introduction is centred and its page title no longer repeats the project name (#98).
|
|
27
|
+
- The website has three sections, Start here, Guides, and Reference, and the sidebar shows only the current one. Each example's guide moved from its README to a website page that shows its code from the repository. The reference is now API docs in YARD's shape, tested against `sig/carton.rbs`, with a page per `carton` command tested against its usage (#90).
|
|
28
|
+
- Renamed the Rails-in-Main example to `rails_with_carton_domain` and aligned both Rails examples around Customers and Billing: domain-owned registration, validation, exact invoice pricing, and persistence through injected repositories. The conventional app keeps Rails adapters in Main; the Engine app owns them in its domain Cartons. Removed the unrelated jobs, updated the private reload fixture, and placed both examples together in the docs (#83).
|
|
29
|
+
|
|
30
|
+
- Expanded Rails-in-Main into a conventional Rails app with migrations, HTML/JSON responses, an Active Job, standard CLI commands, and isolated Billing integration tests (#80, #81).
|
|
31
|
+
|
|
32
|
+
- The Rails-in-Cartons example uses isolated Rails Engines with domain-owned loaders, migrations, HTML views, GlobalID jobs, and an explicit Rails CLI bridge (#79).
|
|
33
|
+
|
|
34
|
+
- The website is now the only user guide and reference: `docs/usage.md` is gone, its remaining details moved to the website, which adds Examples and Rails pages, and the website's Status page is the only place that tracks readiness (#77).
|
|
35
|
+
- Moved the unsupported Master prelude and private Rails reloading experiments into `spikes/`, with dedicated tasks and CI coverage; both Rails examples remain in `examples/`. The reloading spike reuses the Rails-in-Cartons app as a read-only fixture (#76).
|
|
36
|
+
|
|
37
|
+
- Bundler support now starts at 4.0.20, the first release with the gemspec evaluation fix Carton needs inside Boxes. CI and the example bundles use that minimum (#71).
|
|
38
|
+
- `examples/monorepo` keeps its Cartons under `cartons/` instead of `packages/` (#65).
|
|
39
|
+
- `CONTEXT.md` groups the glossary and adds Interior, Gem-shaped Directory, Root Box, User Box, Carton Box, Template Box, Importer, First Importer, Second Copy, Dynamic Import, Helper, Gem Closure, Carton Bundle, and Undeclared Gem. The docs, comments, and `carton-tools` internals use these terms (#65).
|
|
40
|
+
- `docs/internals/root-box-vs-main-box.md` is now `docs/internals/box-names.md`, which records upstream Ruby's Box names and how they were chosen (#65).
|
|
41
|
+
- The examples guide explains each example's purpose and setup. The minimal app now contains only working Imports, with intentional violations in a separate file for static checks. The guides and comments follow `CONTEXT.md`, including Interior, Public Surface, Importer, Carton Bundle, and Helper. The Rails guides share restart guidance, and Bundler and Zeitwerk examples use clearer labels and names (#64).
|
|
42
|
+
|
|
43
|
+
### Removed
|
|
44
|
+
|
|
45
|
+
- Removed `examples/rails_with_carton_domain` and its guide, leaving one Rails example with domain-owned Engines (#100).
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- Sample path gems declare and require Carton instead of relying on the host to preload it; the monorepo's leaf gems also work with ordinary `require` without Boxes enabled. The Rails shop gem includes its database configuration. Example docs distinguish bundle installation from activation and correct the undeclared-import demonstration and Rails class-sharing explanation (#64).
|
|
50
|
+
|
|
5
51
|
## [0.3.0] - 2026-10-18
|
|
6
52
|
|
|
7
|
-
`export` now takes one value, which `import` returns as is. This release also adds the `carton-tools` gem, whose `carton` command checks imports and exports and writes RBS, and makes a Carton's autoloads load in its own Box.
|
|
53
|
+
`export` now takes one value, which `import` returns as is. This release also adds the `carton-tools` gem, whose `carton` command checks imports and exports and writes RBS, and makes a Carton's autoloads load in its own Box (#41, #48, #58, #59).
|
|
8
54
|
|
|
9
55
|
### Breaking
|
|
10
56
|
|
|
11
|
-
- `export` takes one value, which `import` returns as is, and replaces `export_default`: write `export Billing`. Named Exports are removed, since a Ruby module is already a namespace. Export a module of methods instead, or use keywords, which now export a plain Hash: `export version: '1.0'` returns `{ version: '1.0' }`, so read it with `[:version]` or a `=>` pattern. `export` may be called once per Entrypoint, and a Symbol, as in `export def add` or `export :Money`, raises `Carton::ExportError
|
|
12
|
-
- `Carton::Exports` is renamed `Carton::Namespace`, and only Bare Cartons return it.
|
|
57
|
+
- `export` takes one value, which `import` returns as is, and replaces `export_default`: write `export Billing`. Named Exports are removed, since a Ruby module is already a namespace. Export a module of methods instead, or use keywords, which now export a plain Hash: `export version: '1.0'` returns `{ version: '1.0' }`, so read it with `[:version]` or a `=>` pattern. `export` may be called once per Entrypoint, and a Symbol, as in `export def add` or `export :Money`, raises `Carton::ExportError` (#59).
|
|
58
|
+
- `Carton::Exports` is renamed `Carton::Namespace`, and only Bare Cartons return it (#59).
|
|
13
59
|
|
|
14
60
|
### Added
|
|
15
61
|
|
|
16
|
-
- The `carton-tools` gem, with a `carton` command that reads code with Prism and never runs it, so it needs no `RUBY_BOX=1`. Give it the app's files; it follows imports into every Carton they reach. It is released with `carton` at exactly the same version, and `carton` itself stays the runtime only, with no dependencies and no executable.
|
|
17
|
-
- `carton check --strict` fails on warnings as well as errors, and a `# carton:dynamic` comment marks an intentionally dynamic import, such as plugin discovery, to silence its warning.
|
|
18
|
-
- `carton check` follows names bound to imports and reports, as errors, a key an exported Hash does not have, read with `fetch(:nope)` or a `=>` pattern, and a reference to an exported module's private constant (`Billing::Tax`).
|
|
19
|
-
- `bench/scale.rb` (`rake bench`) measures import time and memory as Cartons and bundles grow; [docs/performance.md](docs/performance.md) has the results.
|
|
20
|
-
- `Carton::Namespace#keys` and `#to_h`, and a readable `inspect`: `#<Carton::Namespace add, PI
|
|
21
|
-
- `examples/plugins`, a report runner whose two plugins load bigdecimal 3.3.1 and 4.1.1 in one process from their own bundles, while one plugin's `Float#to_s` patch stays out of the host and the other plugin.
|
|
22
|
-
- `carton rbs` writes `sig/generated/carton.rbs` so Steep knows what each import returns: an exported constant's own type, a record type for a keyword export, or a generated `Carton::Namespace::*` module for a Bare Carton, typed from rbs-inline `#:` annotations and literal values.
|
|
23
|
-
- `carton check` reports missing imports, imports of files inside a Carton, import cycles, `require` calls that load a second copy of an imported Carton, and invalid `export` calls, and exits non-zero on errors.
|
|
24
|
-
- `carton check` treats a gem Carton's gemspec as its manifest: it reports each gem the Carton imports or requires that its gemspec does not declare.
|
|
25
|
-
- `examples/monorepo/test` shows both ways to test a Carton: a unit test that loads `pricing` with plain `require` and tests its private `Rounding`, and an integration test that imports the packages and sees only their exports. `rake example:<name>` runs an example's `test/**/*_test.rb` after `main.rb
|
|
26
|
-
- `examples/rails_in_cartons` declares the `logger` dependency its `shop` Carton requires.
|
|
27
|
-
- `sig/carton.rbs` declares `Carton::NamespaceMethods`, which `Carton::Namespace` includes.
|
|
28
|
-
- The minimal example type-checks with Steep.
|
|
29
|
-
- `examples/zeitwerk` shows a Carton whose loader uses Zeitwerk 2.8 nsfiles (`loader.nsfile = 'ns.rb'`): a lazy namespace defined in `ns.rb` loads in the Carton's Box when the app references it first, and reloads with the Carton's other files. Another Carton runs Zeitwerk 2.7.5, without nsfiles, in the same process.
|
|
30
|
-
- `examples/zeitwerk` shows Zeitwerk with Cartons outside Rails: a Carton using `Zeitwerk::Loader.for_gem`, a separate Zeitwerk per Box, an app loader that ignores a Carton in its directory, eager loading so autoloads never run in an importer's Box, and reloading, which leaves importers' references stale.
|
|
62
|
+
- The `carton-tools` gem, with a `carton` command that reads code with Prism and never runs it, so it needs no `RUBY_BOX=1`. Give it the app's files; it follows imports into every Carton they reach. It is released with `carton` at exactly the same version, and `carton` itself stays the runtime only, with no dependencies and no executable (#41, #58).
|
|
63
|
+
- `carton check --strict` fails on warnings as well as errors, and a `# carton:dynamic` comment marks an intentionally dynamic import, such as plugin discovery, to silence its warning (#53).
|
|
64
|
+
- `carton check` follows names bound to imports and reports, as errors, a key an exported Hash does not have, read with `fetch(:nope)` or a `=>` pattern, and a reference to an exported module's private constant (`Billing::Tax`) (#53, #59).
|
|
65
|
+
- `bench/scale.rb` (`rake bench`) measures import time and memory as Cartons and bundles grow; [docs/performance.md](docs/performance.md) has the results (#50).
|
|
66
|
+
- `Carton::Namespace#keys` and `#to_h`, and a readable `inspect`: `#<Carton::Namespace add, PI>` (#43, #59).
|
|
67
|
+
- `examples/plugins`, a report runner whose two plugins load bigdecimal 3.3.1 and 4.1.1 in one process from their own bundles, while one plugin's `Float#to_s` patch stays out of the host and the other plugin (#45).
|
|
68
|
+
- `carton rbs` writes `sig/generated/carton.rbs` so Steep knows what each import returns: an exported constant's own type, a record type for a keyword export, or a generated `Carton::Namespace::*` module for a Bare Carton, typed from rbs-inline `#:` annotations and literal values (#41, #59).
|
|
69
|
+
- `carton check` reports missing imports, imports of files inside a Carton, import cycles, `require` calls that load a second copy of an imported Carton, and invalid `export` calls, and exits non-zero on errors (#41, #59).
|
|
70
|
+
- `carton check` treats a gem Carton's gemspec as its manifest: it reports each gem the Carton imports or requires that its gemspec does not declare (#42).
|
|
71
|
+
- `examples/monorepo/test` shows both ways to test a Carton: a unit test that loads `pricing` with plain `require` and tests its private `Rounding`, and an integration test that imports the packages and sees only their exports. `rake example:<name>` runs an example's `test/**/*_test.rb` after `main.rb` (#44).
|
|
72
|
+
- `examples/rails_in_cartons` declares the `logger` dependency its `shop` Carton requires (#42).
|
|
73
|
+
- `sig/carton.rbs` declares `Carton::NamespaceMethods`, which `Carton::Namespace` includes (#41, #59).
|
|
74
|
+
- The minimal example type-checks with Steep (#41).
|
|
75
|
+
- `examples/zeitwerk` shows a Carton whose loader uses Zeitwerk 2.8 nsfiles (`loader.nsfile = 'ns.rb'`): a lazy namespace defined in `ns.rb` loads in the Carton's Box when the app references it first, and reloads with the Carton's other files. Another Carton runs Zeitwerk 2.7.5, without nsfiles, in the same process (#60).
|
|
76
|
+
- `examples/zeitwerk` shows Zeitwerk with Cartons outside Rails: a Carton using `Zeitwerk::Loader.for_gem`, a separate Zeitwerk per Box, an app loader that ignores a Carton in its directory, eager loading so autoloads never run in an importer's Box, and reloading, which leaves importers' references stale (#46).
|
|
31
77
|
|
|
32
78
|
### Changed
|
|
33
79
|
|
|
34
|
-
- The README and gem summary present Carton as the package system Ruby has been missing, with the tagline "Real boundaries for Ruby", and compare it with Bundler and Packwerk. The README states what a Box does and does not isolate next to its promises, and says where Carton fits today: an app's or monorepo's domains, plugin hosts with conflicting gem versions, Rails inside Cartons as experimental, and not file-by-file loading. Usage recommends exporting a root module, and the docs show shared imports' consequences early, with a plugin testing recipe. Rails in the Main Box is the recommended Rails setup.
|
|
35
|
-
- A Bare Carton helper named like a method every `Carton::Namespace` has, such as `def fetch` or `def name`, raises `Carton::ExportError`. It used to replace the built-in method, so `namespace.fetch(:fetch)` broke. `carton check` reports these too.
|
|
36
|
-
- A missing `import_relative` target's `LoadError` names the files Carton tried, and a name `import` cannot find says it searched `$LOAD_PATH`, suggesting `import_relative` when a matching file is beside the caller.
|
|
37
|
-
- The Rails examples eager load their Cartons' Zeitwerk loaders, so `rails_in_main` no longer loads `Billing::Invoice` in the Main Box, and both gain a Development section on restarting after changes. Each example README now opens with the problem it solves, and `master_prelude` moved out of the README's examples table to an Experiments note.
|
|
80
|
+
- The README and gem summary present Carton as the package system Ruby has been missing, with the tagline "Real boundaries for Ruby", and compare it with Bundler and Packwerk. The README states what a Box does and does not isolate next to its promises, and says where Carton fits today: an app's or monorepo's domains, plugin hosts with conflicting gem versions, Rails inside Cartons as experimental, and not file-by-file loading. Usage recommends exporting a root module, and the docs show shared imports' consequences early, with a plugin testing recipe. Rails in the Main Box is the recommended Rails setup (#56, #57, #59).
|
|
81
|
+
- A Bare Carton helper named like a method every `Carton::Namespace` has, such as `def fetch` or `def name`, raises `Carton::ExportError`. It used to replace the built-in method, so `namespace.fetch(:fetch)` broke. `carton check` reports these too (#43).
|
|
82
|
+
- A missing `import_relative` target's `LoadError` names the files Carton tried, and a name `import` cannot find says it searched `$LOAD_PATH`, suggesting `import_relative` when a matching file is beside the caller (#43).
|
|
83
|
+
- The Rails examples eager load their Cartons' Zeitwerk loaders, so `rails_in_main` no longer loads `Billing::Invoice` in the Main Box, and both gain a Development section on restarting after changes. Each example README now opens with the problem it solves, and `master_prelude` moved out of the README's examples table to an Experiments note (#47).
|
|
38
84
|
|
|
39
85
|
### Fixed
|
|
40
86
|
|
|
41
|
-
- An Entrypoint that starts a thread which imports another Carton, and waits for it, no longer blocks forever. The import cache held one global lock while running every Entrypoint; it now locks only its bookkeeping and makes a thread wait only for an Entrypoint another thread is loading.
|
|
42
|
-
- `carton check` no longer reports `export` calls in different branches of one `if`, `unless`, or `case` as duplicates, since only one branch runs.
|
|
43
|
-
- A shared gem Carton raises `Carton::ImportError` when a later importer locks a different version of a gem it loaded without declaring, such as an undeclared gem the first importer's lockfile allowed, including one it required lazily after its import. Before, only its declared dependencies were compared, so it could be shared silently.
|
|
44
|
-
- A Bare Carton imported from a gem no longer exposes `Bundler`, `Psych`, `Date`, or `DateTime`, which Carton's gem activation loaded into its Box.
|
|
45
|
-
- A gem Carton's default-gem dependencies, such as `json` and `date`, keep the importer's activated version when a newer one is installed.
|
|
46
|
-
- The master_prelude example and Box docs no longer claim the Main Box is unaffected: patches loaded into Master can reach Main and existing Boxes, depending on Ruby's copy-on-write state.
|
|
47
|
-
- An autoload a Carton defines, such as a lazy Zeitwerk constant, loads in the Carton's Box even when an importer references it first. It used to run in the importer's Box, often the Main Box, or raise `NameError` when the importer used another name. Autoloaded files also go through the Box's `Kernel#require`, so Zeitwerk's `on_load` callbacks and reloading see them.
|
|
87
|
+
- An Entrypoint that starts a thread which imports another Carton, and waits for it, no longer blocks forever. The import cache held one global lock while running every Entrypoint; it now locks only its bookkeeping and makes a thread wait only for an Entrypoint another thread is loading (#55).
|
|
88
|
+
- `carton check` no longer reports `export` calls in different branches of one `if`, `unless`, or `case` as duplicates, since only one branch runs (#53).
|
|
89
|
+
- A shared gem Carton raises `Carton::ImportError` when a later importer locks a different version of a gem it loaded without declaring, such as an undeclared gem the first importer's lockfile allowed, including one it required lazily after its import. Before, only its declared dependencies were compared, so it could be shared silently (#52).
|
|
90
|
+
- A Bare Carton imported from a gem no longer exposes `Bundler`, `Psych`, `Date`, or `DateTime`, which Carton's gem activation loaded into its Box (#52).
|
|
91
|
+
- A gem Carton's default-gem dependencies, such as `json` and `date`, keep the importer's activated version when a newer one is installed (#52).
|
|
92
|
+
- The master_prelude example and Box docs no longer claim the Main Box is unaffected: patches loaded into Master can reach Main and existing Boxes, depending on Ruby's copy-on-write state (#52).
|
|
93
|
+
- An autoload a Carton defines, such as a lazy Zeitwerk constant, loads in the Carton's Box even when an importer references it first. It used to run in the importer's Box, often the Main Box, or raise `NameError` when the importer used another name. Autoloaded files also go through the Box's `Kernel#require`, so Zeitwerk's `on_load` callbacks and reloading see them (#48).
|
|
48
94
|
|
|
49
95
|
## [0.2.0] - 2026-10-17
|
|
50
96
|
|
|
51
|
-
Cartons are now gem-sized or domain-sized units: a file plus the directory named after it. This release also redesigns exports, shares imports across the process, adds gem and Bundler support, and requires Ruby 4.0.7.
|
|
97
|
+
Cartons are now gem-sized or domain-sized units: a file plus the directory named after it. This release also redesigns exports, shares imports across the process, adds gem and Bundler support, and requires Ruby 4.0.7 (#9, #10, #11, #14, #15, #17, #18, #19, #20, #36).
|
|
52
98
|
|
|
53
99
|
### Breaking
|
|
54
100
|
|
|
55
|
-
- A Carton is now a file plus the directory named after it, the shape of a gem's `lib/`: `billing.rb` and everything under `billing/`, including Zeitwerk implicit-namespace directories. Importing a file inside a Carton, such as `import 'billing/invoice'`, raises `Carton::ImportError` naming the import to use. It used to load a second copy of the file in its own Box, so its classes did not match the Carton's.
|
|
56
|
-
- Carton now requires Ruby 4.0.7 or later, enforced by the gemspec, and Bundler support needs Bundler 4.0.23 or later.
|
|
57
|
-
- `export` can be called many times per file, and each call adds to one Export Namespace. A capitalized Symbol exports a constant (`export :Money`), other Symbols export methods (`export :add`, or `export def add ...`), and keywords export plain values. Replace `export add: method(:add)` with `export :add`. A missing name raises `NameError`, and a name exported twice raises `Carton::ExportError
|
|
58
|
-
- Exported methods forward when called (`exports.add(...)`), while `[]`, `fetch`, and destructuring return the `Method` object. Keyword values are no longer invoked: `export handler: -> {}` makes `exports.handler` return the lambda.
|
|
59
|
-
- `export_default` may be called only once per file and cannot be combined with `export`; both raise `Carton::ExportError`. Put other names inside the default value.
|
|
60
|
-
- Importing a file with no exports (a Bare Carton) returns a `Carton::Exports` of the constants and top-level helpers it defines, instead of its `Carton::Box`. Importers can no longer `eval`, `require`, or read `load_path` inside another Carton. Helpers are callable directly (`toolbox.helper`), lookups return their `Method` object (`toolbox.fetch(:helper).call`), and constants are read when the import finishes. `Carton::Box` is now a private constant.
|
|
61
|
-
- Imports are shared across the process. Each Carton file loads once, and every later `import` or `import_relative` of it returns the same value, so sibling Cartons share classes and Carton state no longer resets per import. A Carton that imports itself raises `Carton::ImportError
|
|
62
|
-
- `import 'name'` no longer checks the current directory before `$LOAD_PATH`, so results no longer depend on where the app was started. Use an absolute path, `import_relative`, or add the directory to `$LOAD_PATH
|
|
63
|
-
- `Carton.with_bundle { require 'bundler/setup' }` is replaced by `Carton.setup_bundle`. Pass groups as with `Bundler.setup` (`Carton.setup_bundle(:default, :test)`) and choose a Gemfile with `gemfile:`. Bundler errors raise instead of exiting the process.
|
|
101
|
+
- A Carton is now a file plus the directory named after it, the shape of a gem's `lib/`: `billing.rb` and everything under `billing/`, including Zeitwerk implicit-namespace directories. Importing a file inside a Carton, such as `import 'billing/invoice'`, raises `Carton::ImportError` naming the import to use. It used to load a second copy of the file in its own Box, so its classes did not match the Carton's (#36).
|
|
102
|
+
- Carton now requires Ruby 4.0.7 or later, enforced by the gemspec, and Bundler support needs Bundler 4.0.23 or later (#11).
|
|
103
|
+
- `export` can be called many times per file, and each call adds to one Export Namespace. A capitalized Symbol exports a constant (`export :Money`), other Symbols export methods (`export :add`, or `export def add ...`), and keywords export plain values. Replace `export add: method(:add)` with `export :add`. A missing name raises `NameError`, and a name exported twice raises `Carton::ExportError` (#9, #19).
|
|
104
|
+
- Exported methods forward when called (`exports.add(...)`), while `[]`, `fetch`, and destructuring return the `Method` object. Keyword values are no longer invoked: `export handler: -> {}` makes `exports.handler` return the lambda (#9).
|
|
105
|
+
- `export_default` may be called only once per file and cannot be combined with `export`; both raise `Carton::ExportError`. Put other names inside the default value (#20).
|
|
106
|
+
- Importing a file with no exports (a Bare Carton) returns a `Carton::Exports` of the constants and top-level helpers it defines, instead of its `Carton::Box`. Importers can no longer `eval`, `require`, or read `load_path` inside another Carton. Helpers are callable directly (`toolbox.helper`), lookups return their `Method` object (`toolbox.fetch(:helper).call`), and constants are read when the import finishes. `Carton::Box` is now a private constant (#17).
|
|
107
|
+
- Imports are shared across the process. Each Carton file loads once, and every later `import` or `import_relative` of it returns the same value, so sibling Cartons share classes and Carton state no longer resets per import. A Carton that imports itself raises `Carton::ImportError` (#10).
|
|
108
|
+
- `import 'name'` no longer checks the current directory before `$LOAD_PATH`, so results no longer depend on where the app was started. Use an absolute path, `import_relative`, or add the directory to `$LOAD_PATH` (#13).
|
|
109
|
+
- `Carton.with_bundle { require 'bundler/setup' }` is replaced by `Carton.setup_bundle`. Pass groups as with `Bundler.setup` (`Carton.setup_bundle(:default, :test)`) and choose a Gemfile with `gemfile:`. Bundler errors raise instead of exiting the process (#11).
|
|
64
110
|
|
|
65
111
|
### Added
|
|
66
112
|
|
|
67
|
-
- `import_relative` and absolute `import` accept a gem-shaped directory: `import_relative 'components/billing'` loads `components/billing/lib/billing.rb` with that `lib/` on the Carton's load path.
|
|
68
|
-
- `Carton::Error`, the base class of every error Carton raises itself, with `Carton::ImportError` (circular imports, gem version mismatches) and `Carton::ExportError` (repeated or conflicting exports). They replace plain `RuntimeError`s and `ArgumentError`s, and are the same classes in every Box. `Carton::GemfileNotFound` is now a `Carton::Error
|
|
69
|
-
- `Carton.setup_bundle(*groups, gemfile: nil)` sets up a Carton's own bundle. Without `gemfile:` it searches upward from the calling file for `gems.rb` or `Gemfile`. It clears any stale `BUNDLE_LOCKFILE` and restores all of `ENV` after setup, so a Carton's bundle variables never reach the rest of the process. `Carton::GemfileNotFound` is raised when there is no Gemfile.
|
|
70
|
-
- Public RBS signatures in `sig/carton.rbs
|
|
71
|
-
- `examples/monorepo`: packages as path gems in one Gemfile, imported by gem name.
|
|
72
|
-
- `examples/rails_in_main`: a Rails API app whose domain logic is a Zeitwerk-loaded path-gem Carton.
|
|
73
|
-
- `examples/rails_in_cartons`: Rails loaded once in a framework Carton, with models and controllers spread across domain Cartons. The Main Box runs only Rack.
|
|
74
|
-
- `examples/master_prelude`: an optional, unsupported hack that loads Active Support's core extensions into Ruby's Master Box so every Carton inherits them.
|
|
113
|
+
- `import_relative` and absolute `import` accept a gem-shaped directory: `import_relative 'components/billing'` loads `components/billing/lib/billing.rb` with that `lib/` on the Carton's load path (#36).
|
|
114
|
+
- `Carton::Error`, the base class of every error Carton raises itself, with `Carton::ImportError` (circular imports, gem version mismatches) and `Carton::ExportError` (repeated or conflicting exports). They replace plain `RuntimeError`s and `ArgumentError`s, and are the same classes in every Box. `Carton::GemfileNotFound` is now a `Carton::Error` (#35).
|
|
115
|
+
- `Carton.setup_bundle(*groups, gemfile: nil)` sets up a Carton's own bundle. Without `gemfile:` it searches upward from the calling file for `gems.rb` or `Gemfile`. It clears any stale `BUNDLE_LOCKFILE` and restores all of `ENV` after setup, so a Carton's bundle variables never reach the rest of the process. `Carton::GemfileNotFound` is raised when there is no Gemfile (#11, #12).
|
|
116
|
+
- Public RBS signatures in `sig/carton.rbs` ([0b7d16c](https://github.com/dtcristo/carton/commit/0b7d16c6e251931759edbc65f518b11377635f5d)).
|
|
117
|
+
- `examples/monorepo`: packages as path gems in one Gemfile, imported by gem name (#33).
|
|
118
|
+
- `examples/rails_in_main`: a Rails API app whose domain logic is a Zeitwerk-loaded path-gem Carton (#21, #23).
|
|
119
|
+
- `examples/rails_in_cartons`: Rails loaded once in a framework Carton, with models and controllers spread across domain Cartons. The Main Box runs only Rack (#22, #23).
|
|
120
|
+
- `examples/master_prelude`: an optional, unsupported hack that loads Active Support's core extensions into Ruby's Master Box so every Carton inherits them (#24).
|
|
75
121
|
|
|
76
122
|
### Changed
|
|
77
123
|
|
|
78
|
-
- `export` and `export_default` only declare exports from the imported Entrypoint. Under plain `require`, or in other files a Carton requires, they validate their arguments and do nothing, so Carton-aware files and gems also load with `require
|
|
79
|
-
- Importing a file from an activated gem activates that gem and its runtime dependencies in the Carton's Box at the caller's versions. When the caller ran Bundler setup, the Box is also restricted to the caller's bundle: every locked gem loads at its locked version, and gems outside the lockfile raise `LoadError
|
|
80
|
-
- `import`, `import_relative`, `export`, and `export_default` are private Kernel methods, like `require`, so `"str".import(...)` raises `NoMethodError` and objects no longer `respond_to?` them.
|
|
81
|
-
- `import 'name'` resolves the feature in the caller's Box and carries only the matching load-path entry into the imported Box, instead of copying the caller's whole `$LOAD_PATH
|
|
82
|
-
- `RUBY_BOX=1 bundle exec` works, including Gemfiles that evaluate a gemspec. Carton no longer patches RubyGems or Bundler.
|
|
124
|
+
- `export` and `export_default` only declare exports from the imported Entrypoint. Under plain `require`, or in other files a Carton requires, they validate their arguments and do nothing, so Carton-aware files and gems also load with `require` (#18).
|
|
125
|
+
- Importing a file from an activated gem activates that gem and its runtime dependencies in the Carton's Box at the caller's versions. When the caller ran Bundler setup, the Box is also restricted to the caller's bundle: every locked gem loads at its locked version, and gems outside the lockfile raise `LoadError` (#14, #15).
|
|
126
|
+
- `import`, `import_relative`, `export`, and `export_default` are private Kernel methods, like `require`, so `"str".import(...)` raises `NoMethodError` and objects no longer `respond_to?` them (#8).
|
|
127
|
+
- `import 'name'` resolves the feature in the caller's Box and carries only the matching load-path entry into the imported Box, instead of copying the caller's whole `$LOAD_PATH` ([110dfb6](https://github.com/dtcristo/carton/commit/110dfb68c7488e2e32de3e71c01fcecccbc893f8)).
|
|
128
|
+
- `RUBY_BOX=1 bundle exec` works, including Gemfiles that evaluate a gemspec. Carton no longer patches RubyGems or Bundler (#11).
|
|
83
129
|
|
|
84
130
|
### Fixed
|
|
85
131
|
|
|
86
|
-
- Importing without `RUBY_BOX=1` raises `Carton::ImportError` explaining how to enable Boxes, instead of `NoMethodError` on `nil
|
|
87
|
-
- A missing `import_relative` target raises `LoadError` instead of loading a same-named feature from the load path (such as the stdlib `json`), and a missing absolute `import` raises `LoadError` instead of `Errno::ENOENT
|
|
88
|
-
- `export_default` keeps Hash, `nil`, and `false` values as Default Exports instead of treating a Hash as an Export Namespace.
|
|
89
|
-
- A gem Carton imported from another Carton's Box gets its dependencies. Activation used to write to the importing Carton's `$LOAD_PATH`, so the gem's `require` of a dependency raised `LoadError
|
|
90
|
-
- A gem Carton's dependencies load at the caller's versions instead of the newest installed ones. A later import whose caller has different versions activated raises instead of reusing the cached Carton.
|
|
91
|
-
- Bare Carton lookup only finds constants and helpers the Entrypoint defines. Keys are no longer evaluated as Ruby expressions, and `key?` no longer calls helpers.
|
|
92
|
-
- Hash-rest patterns (`=> { **rest }`) return every Named Export, or every member of a Bare Carton's Public Surface, instead of an empty hash.
|
|
132
|
+
- Importing without `RUBY_BOX=1` raises `Carton::ImportError` explaining how to enable Boxes, instead of `NoMethodError` on `nil` (#35).
|
|
133
|
+
- A missing `import_relative` target raises `LoadError` instead of loading a same-named feature from the load path (such as the stdlib `json`), and a missing absolute `import` raises `LoadError` instead of `Errno::ENOENT` (#30).
|
|
134
|
+
- `export_default` keeps Hash, `nil`, and `false` values as Default Exports instead of treating a Hash as an Export Namespace (#4).
|
|
135
|
+
- A gem Carton imported from another Carton's Box gets its dependencies. Activation used to write to the importing Carton's `$LOAD_PATH`, so the gem's `require` of a dependency raised `LoadError` (#15).
|
|
136
|
+
- A gem Carton's dependencies load at the caller's versions instead of the newest installed ones. A later import whose caller has different versions activated raises instead of reusing the cached Carton (#14).
|
|
137
|
+
- Bare Carton lookup only finds constants and helpers the Entrypoint defines. Keys are no longer evaluated as Ruby expressions, and `key?` no longer calls helpers (#5).
|
|
138
|
+
- Hash-rest patterns (`=> { **rest }`) return every Named Export, or every member of a Bare Carton's Public Surface, instead of an empty hash (#6).
|
|
93
139
|
|
|
94
140
|
## [0.1.0] - 2026-04-16
|
|
95
141
|
|
|
96
|
-
Initial release.
|
|
142
|
+
Initial release ([9925397](https://github.com/dtcristo/carton/commit/99253978dd73b70698341d04314e3ccf86b37c16)).
|
|
97
143
|
|
|
98
|
-
[Unreleased]: https://github.com/dtcristo/carton/compare/v0.
|
|
144
|
+
[Unreleased]: https://github.com/dtcristo/carton/compare/v0.4.0...HEAD
|
|
145
|
+
[0.4.0]: https://github.com/dtcristo/carton/compare/v0.3.0...v0.4.0
|
|
99
146
|
[0.3.0]: https://github.com/dtcristo/carton/compare/v0.2.0...v0.3.0
|
|
100
147
|
[0.2.0]: https://github.com/dtcristo/carton/compare/v0.1.0...v0.2.0
|
|
101
148
|
[0.1.0]: https://github.com/dtcristo/carton/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -10,132 +10,77 @@
|
|
|
10
10
|
</p>
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Carton is the package system Ruby has been missing. Split code into Cartons that import and export like modules. Each Carton runs in its own [`Ruby::Box`](https://docs.ruby-lang.org/en/4.0/Ruby/Box.html), so Ruby enforces the boundary while the code runs:
|
|
13
|
+
Split your app into Cartons with explicit imports and exports. Each has its own dependencies and runs in a separate [`Ruby::Box`](https://docs.ruby-lang.org/en/4.0/Ruby/Box.html), so Ruby enforces the boundaries at runtime:
|
|
16
14
|
|
|
17
15
|
- other Cartons reach **only what a Carton exports**;
|
|
18
|
-
- **monkey patches to builtin classes
|
|
16
|
+
- **monkey patches to builtin classes** stay in the Carton that makes them;
|
|
19
17
|
- two Cartons can use **different versions of the same gem**.
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
A Box does not isolate everything. Classes and objects a Carton exports are shared, not copied, so reopening an exported class or changing a shared object affects every importer. A plain `require` of a Carton's files loads a second copy outside its boundary, which `carton check` reports. And each Carton loads once per process: its state lasts as long as the process, and its first importer fixes its gem versions.
|
|
24
|
-
|
|
25
|
-
## Why Carton
|
|
26
|
-
|
|
27
|
-
| | Bundler | [Packwerk](https://github.com/Shopify/packwerk) | Carton |
|
|
28
|
-
| --- | --- | --- | --- |
|
|
29
|
-
| Boundaries inside an app | No | Checked statically, in CI | Enforced at runtime, and checked statically by `carton check` |
|
|
30
|
-
| Private internals | No | Privacy checks removed in 3.0 | Only exports cross; `private_constant` hides the rest |
|
|
31
|
-
| Constants loaded dynamically | n/a | Not seen | Inside the boundary like any other |
|
|
32
|
-
| Gem versions | One per app | One per app | One per Carton |
|
|
33
|
-
| Monkey patches to builtins | Global | Global | Stay in their Carton |
|
|
34
|
-
| Unit of distribution | A gem | A pack, which stays in the app | A gem-shaped Carton, which can become a gem |
|
|
35
|
-
|
|
36
|
-
Static checks only see what they can parse. Packwerk's authors note that a package with zero violations "may actually crash with name errors when its code is executed" ([A Packwerk Retrospective](https://shopify.engineering/a-packwerk-retrospective)). A Carton's boundary is the Box it runs in, so what `carton check` reports ahead of time is also what Ruby enforces.
|
|
37
|
-
|
|
38
|
-
## Where it fits today
|
|
19
|
+
Every imported Entrypoint must call `export` exactly once. Loading without an Export raises `Carton::ExportError`; importing returns the exported value itself.
|
|
39
20
|
|
|
40
|
-
|
|
41
|
-
- **Plugin hosts whose plugins need conflicting gem versions**, which plain Ruby cannot load together; see [examples/plugins](examples/plugins/README.md).
|
|
42
|
-
- **Rails inside Cartons** works but is experimental: there is no reloading yet, and domain Cartons see Active Support's core extensions only if they load them ([examples/rails_in_cartons](examples/rails_in_cartons/README.md)).
|
|
43
|
-
- **Not yet file by file.** Each Carton costs about 60 ms and 2.4 MB ([performance.md](docs/performance.md)), most of it `Ruby::Box.new`, so aim for tens of Cartons, not hundreds.
|
|
21
|
+
A Carton is shaped like a gem: `billing.rb` plus the `billing/` directory beside it. There is no manifest, and with a gemspec a Carton can leave the app as a gem.
|
|
44
22
|
|
|
45
23
|
## Quick start
|
|
46
24
|
|
|
47
|
-
Carton needs Ruby 4.0.7+ with Boxes enabled (`RUBY_BOX=1`).
|
|
25
|
+
Carton needs Ruby 4.0.7+ with Boxes enabled (`RUBY_BOX=1`).
|
|
48
26
|
|
|
49
27
|
```sh
|
|
50
28
|
gem install carton # or add `gem 'carton'` to your Gemfile
|
|
51
29
|
```
|
|
52
30
|
|
|
53
|
-
|
|
31
|
+
```ruby
|
|
32
|
+
# tax.rb: the tax Carton
|
|
33
|
+
module Tax
|
|
34
|
+
def self.on(total) = total / 10
|
|
35
|
+
end
|
|
54
36
|
|
|
55
|
-
|
|
56
|
-
billing.rb # the Entrypoint
|
|
57
|
-
billing/invoice.rb # inside the Carton
|
|
37
|
+
export Tax # the one value importers get
|
|
58
38
|
```
|
|
59
39
|
|
|
60
40
|
```ruby
|
|
61
|
-
# billing.rb
|
|
62
|
-
|
|
41
|
+
# billing.rb: the billing Carton
|
|
42
|
+
Tax = import_relative 'tax' # billing's import, invisible to importers
|
|
63
43
|
|
|
64
44
|
module Billing
|
|
65
|
-
def self.invoice(total) =
|
|
45
|
+
def self.invoice(total) = total + Tax.on(total)
|
|
66
46
|
end
|
|
67
47
|
|
|
68
48
|
export Billing
|
|
69
49
|
```
|
|
70
50
|
|
|
71
|
-
Import it:
|
|
72
|
-
|
|
73
51
|
```ruby
|
|
74
52
|
# main.rb
|
|
75
53
|
require 'carton'
|
|
76
54
|
|
|
77
55
|
Billing = import_relative 'billing'
|
|
78
|
-
Billing.invoice(
|
|
56
|
+
Billing.invoice(100) # => 110
|
|
57
|
+
Tax # NameError: billing's import
|
|
79
58
|
```
|
|
80
59
|
|
|
81
60
|
```sh
|
|
82
61
|
RUBY_BOX=1 ruby main.rb
|
|
83
62
|
```
|
|
84
63
|
|
|
85
|
-
`import_relative 'billing/invoice'` raises `Carton::ImportError`: the file is inside the `billing` Carton, so only `billing` can be imported. `import 'name'` searches `$LOAD_PATH` like `require`, so gems and path gems import by name, and a gem-shaped directory (`components/billing/lib/billing.rb`) imports by its path. Each Carton loads once per process, and every later import returns the same value.
|
|
86
|
-
|
|
87
|
-
## Gems and Bundler
|
|
88
|
-
|
|
89
|
-
A Carton can set up its own bundle, isolated from the app's:
|
|
90
|
-
|
|
91
|
-
```ruby
|
|
92
|
-
# cartons/reports/lib/reports.rb
|
|
93
|
-
Carton.setup_bundle # uses cartons/reports/Gemfile
|
|
94
|
-
require 'bigdecimal'
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
A gem in the app's bundle imports as a Carton and gets its dependencies at the app's locked versions. For a monorepo, make each package a path gem in one Gemfile and import it by name; see [examples/monorepo](examples/monorepo/README.md).
|
|
98
|
-
|
|
99
|
-
## Types and checks
|
|
100
|
-
|
|
101
|
-
The `carton` command, in the [`carton-tools`](carton-tools/README.md) gem, reads code without running it; add `gem 'carton-tools', group: :development` beside `carton`. `carton rbs main.rb` writes RBS so [Steep](https://github.com/soutaro/steep) knows what each import returns, and `carton check main.rb` reports missing imports, cycles, `require` calls that reach into a Carton, and gems a gem Carton uses without declaring them. See [Types and checks](docs/usage.md#types-and-checks).
|
|
102
|
-
|
|
103
64
|
## Status
|
|
104
65
|
|
|
105
|
-
Carton is experimental, and so is `Ruby::Box`.
|
|
66
|
+
Carton is experimental, and so is `Ruby::Box`. Box isolates definitions and load state, not objects, so it is not a security boundary, and classes a Carton exports are shared with every importer. [Why Carton](https://carton.dtcristo.com/start/why-carton/) compares it with Bundler and Packwerk and covers where it fits today.
|
|
106
67
|
|
|
107
68
|
## Docs
|
|
108
69
|
|
|
109
|
-
|
|
110
|
-
- [design.md](docs/design.md): how an import runs
|
|
111
|
-
- [performance.md](docs/performance.md): import time and memory as Cartons and bundles grow
|
|
112
|
-
- [todo.md](docs/todo.md): planned work and open questions
|
|
113
|
-
- [internals/](docs/internals/): how `Ruby::Box`, RubyGems, and Bundler work, and upstream plans
|
|
114
|
-
- [CONTEXT.md](CONTEXT.md): the glossary
|
|
70
|
+
Guides and reference live at [carton.dtcristo.com](https://carton.dtcristo.com) (source in [website/](website/README.md)). For contributors:
|
|
115
71
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
| --- | --- |
|
|
120
|
-
| [minimal](examples/minimal/README.md) | A Carton with its own directory, a single-file Carton, the error for importing a file inside a Carton, and Steep types |
|
|
121
|
-
| [monorepo](examples/monorepo/README.md) | Packages as path gems in one Gemfile, imported by gem name |
|
|
122
|
-
| [rails_in_main](examples/rails_in_main/README.md) | Rails in the Main Box, domain logic in a Carton: the recommended way to use Carton with Rails |
|
|
123
|
-
| [plugins](examples/plugins/README.md) | A plugin host whose plugins need incompatible `bigdecimal` versions, each with its own bundle, and a `Float` patch that stays in one plugin |
|
|
124
|
-
| [gems](examples/gems/README.md) | Two Cartons using different `bigdecimal` versions without Bundler |
|
|
125
|
-
| [bundler](examples/bundler/README.md) | Cartons with their own Bundler setup, plus a path gem |
|
|
126
|
-
| [zeitwerk](examples/zeitwerk/README.md) | Zeitwerk loaders inside Cartons and in the app, eager loading, and what reloading a Carton does to importers |
|
|
127
|
-
| [rails_in_cartons](examples/rails_in_cartons/README.md) | Experimental: Rails loaded once in a Carton, models and controllers in domain Cartons |
|
|
72
|
+
- [design.md](docs/design.md), [performance.md](docs/performance.md), [todo.md](docs/todo.md), and [internals/](docs/internals/)
|
|
73
|
+
- [CONTEXT.md](CONTEXT.md): the glossary
|
|
74
|
+
- [Examples](https://carton.dtcristo.com/guides/examples/): runnable apps, starting with [minimal](https://carton.dtcristo.com/guides/examples/minimal/); run one with `bundle exec rake example:<name>`
|
|
128
75
|
|
|
129
|
-
|
|
76
|
+
The `carton` command, for RBS and boundary checks, is in the [`carton-tools`](carton-tools/README.md) gem.
|
|
130
77
|
|
|
131
78
|
## Development
|
|
132
79
|
|
|
133
80
|
```sh
|
|
134
81
|
bundle install
|
|
135
|
-
bundle exec rake
|
|
136
|
-
bundle exec rake
|
|
137
|
-
bundle exec rake example:monorepo # one example
|
|
138
|
-
bundle exec rake format # format with Syntax Tree
|
|
82
|
+
bundle exec rake # tests, RuboCop, and every example
|
|
83
|
+
bundle exec rake format # format with Syntax Tree
|
|
139
84
|
```
|
|
140
85
|
|
|
141
86
|
## License
|
data/lib/carton/box.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Carton
|
|
4
|
-
#
|
|
5
|
-
# a `Ruby::Box` rather than subclassing it, because Ruby
|
|
6
|
-
# the Box that registered them only for a plain
|
|
7
|
-
# finds the Import from `Ruby::Box.current`
|
|
4
|
+
# A Carton Box: one Import's state, attached to the plain `Ruby::Box` it
|
|
5
|
+
# runs in. It wraps a `Ruby::Box` rather than subclassing it, because Ruby
|
|
6
|
+
# keeps autoloads in the Box that registered them only for a plain
|
|
7
|
+
# `Ruby::Box`. Code in any Box finds the Import from `Ruby::Box.current`
|
|
8
|
+
# with `Box.for`.
|
|
8
9
|
class Box
|
|
9
10
|
ACTIVATE_GEMS = File.expand_path('activate_gems.rb', __dir__)
|
|
10
11
|
private_constant :ACTIVATE_GEMS
|
|
@@ -32,10 +33,10 @@ module Carton
|
|
|
32
33
|
private
|
|
33
34
|
|
|
34
35
|
# The whole Import lifecycle, in order: boot Carton in this Box, load the
|
|
35
|
-
# target, then
|
|
36
|
-
#
|
|
37
|
-
def import_target(target,
|
|
38
|
-
configure_for_import(
|
|
36
|
+
# target, then return its Export. Runtime calls only this, so the ordering
|
|
37
|
+
# lives here.
|
|
38
|
+
def import_target(target, library:, import_cache:)
|
|
39
|
+
configure_for_import(library:, import_cache:)
|
|
39
40
|
load_import(
|
|
40
41
|
target.feature,
|
|
41
42
|
load_path: target.load_path,
|
|
@@ -43,13 +44,14 @@ module Carton
|
|
|
43
44
|
locked_gems: target.locked_gems,
|
|
44
45
|
)
|
|
45
46
|
record_used_gems(target)
|
|
46
|
-
|
|
47
|
+
exported_value
|
|
47
48
|
end
|
|
48
49
|
|
|
49
|
-
# The gems this Carton depends on: its
|
|
50
|
-
# files it has loaded, such as an
|
|
51
|
-
# allows. A later
|
|
52
|
-
# Carton, so they are read again at each check, after any lazy
|
|
50
|
+
# The gems this Carton depends on: its Gem Closure, plus the locked gems
|
|
51
|
+
# whose files it has loaded, such as an Undeclared Gem the Importer's
|
|
52
|
+
# lockfile allows. A later Importer must agree on these versions to share
|
|
53
|
+
# the Carton, so they are read again at each check, after any lazy
|
|
54
|
+
# requires.
|
|
53
55
|
def record_used_gems(target)
|
|
54
56
|
target.gem_usage = -> { used_gems(target) }
|
|
55
57
|
end
|
|
@@ -63,7 +65,7 @@ module Carton
|
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
# `Ruby::Box#eval` does not switch the loading Box, so during a nested
|
|
66
|
-
# Import `$LOADED_FEATURES` there would be the
|
|
68
|
+
# Import `$LOADED_FEATURES` there would be the Importer's. A file loaded
|
|
67
69
|
# in this Box reads this Box's.
|
|
68
70
|
def loaded_features
|
|
69
71
|
@box.eval("load #{LOADED_FEATURES.inspect}")
|
|
@@ -76,31 +78,25 @@ module Carton
|
|
|
76
78
|
@loaded_features = features
|
|
77
79
|
end
|
|
78
80
|
|
|
79
|
-
#
|
|
80
|
-
# Carton's own
|
|
81
|
-
# `import_relative`, and `export`. Nested imports reuse the
|
|
81
|
+
# New Boxes copy Master, so they start without the Importer's gems. Load
|
|
82
|
+
# Carton's own library so the imported file gets `import`,
|
|
83
|
+
# `import_relative`, and `export`. Nested imports reuse the Importer's
|
|
82
84
|
# process-wide import cache.
|
|
83
|
-
def configure_for_import(
|
|
85
|
+
def configure_for_import(library:, import_cache:)
|
|
84
86
|
@import_cache = import_cache
|
|
85
87
|
reset_export
|
|
86
|
-
add_import_load_path(File.dirname(
|
|
87
|
-
require_in_box(
|
|
88
|
+
add_import_load_path(File.dirname(library))
|
|
89
|
+
require_in_box(library)
|
|
88
90
|
share_error_classes
|
|
89
91
|
self
|
|
90
92
|
end
|
|
91
93
|
|
|
92
|
-
# Activate the
|
|
94
|
+
# Activate the Importer's gems for the target, then load it in this Box.
|
|
93
95
|
def load_import(feature, load_path: nil, gems: [], locked_gems: nil)
|
|
94
96
|
activate_gems(gems, locked_gems) unless gems.empty?
|
|
95
97
|
add_import_load_path(load_path) if load_path
|
|
96
|
-
# After activation, which loads Bundler and Psych here: a Bare
|
|
97
|
-
# Carton's surface is only what its Entrypoint adds.
|
|
98
|
-
snapshot_import_support
|
|
99
|
-
|
|
100
98
|
@entrypoint = real_path(feature)
|
|
101
|
-
|
|
102
|
-
discover_public_entries
|
|
103
|
-
result
|
|
99
|
+
require_in_box(feature)
|
|
104
100
|
end
|
|
105
101
|
|
|
106
102
|
attr_reader :import_cache, :pending_gems
|
|
@@ -125,7 +121,7 @@ module Carton
|
|
|
125
121
|
# Box methods run in the Box that defined them, so this Box's own copy of
|
|
126
122
|
# Carton activates gems in this Box's RubyGems. `$LOAD_PATH` resolves to
|
|
127
123
|
# the Box of the file being loaded, which during a nested Import is the
|
|
128
|
-
#
|
|
124
|
+
# Importer, so activation runs from a file required here.
|
|
129
125
|
def activate_gems(gems, locked_gems)
|
|
130
126
|
@pending_gems = [gems, locked_gems]
|
|
131
127
|
require_in_box(ACTIVATE_GEMS)
|
|
@@ -135,7 +131,7 @@ module Carton
|
|
|
135
131
|
|
|
136
132
|
# This Box just loaded its own copy of Carton, whose error classes are new
|
|
137
133
|
# objects that `rescue Carton::Error` in another Box would never match.
|
|
138
|
-
# Point them at the
|
|
134
|
+
# Point them at the Importer's classes, so one rescue catches errors
|
|
139
135
|
# raised in any Carton.
|
|
140
136
|
def share_error_classes
|
|
141
137
|
carton = @box.const_get(:Carton, false)
|
|
@@ -150,7 +146,7 @@ module Carton
|
|
|
150
146
|
load_path.unshift(path) unless load_path.include?(path)
|
|
151
147
|
end
|
|
152
148
|
|
|
153
|
-
# Only the imported Entrypoint declares this Carton's
|
|
149
|
+
# Only the imported Entrypoint declares this Carton's Export. Other files
|
|
154
150
|
# it requires may call `export` for their own imports; here it is inert.
|
|
155
151
|
def entrypoint?(file)
|
|
156
152
|
!@entrypoint.nil? && real_path(file) == @entrypoint
|
|
@@ -170,40 +166,18 @@ module Carton
|
|
|
170
166
|
@export = value
|
|
171
167
|
end
|
|
172
168
|
|
|
173
|
-
#
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
169
|
+
# Every Entrypoint deliberately chooses the value its Importers receive.
|
|
170
|
+
def exported_value
|
|
171
|
+
if NO_EXPORT.equal?(@export)
|
|
172
|
+
raise ExportError,
|
|
173
|
+
"#{@entrypoint} must call export: export a module or another value"
|
|
174
|
+
end
|
|
179
175
|
|
|
180
|
-
|
|
181
|
-
constants =
|
|
182
|
-
@public_constant_names.to_h do |name|
|
|
183
|
-
[name, @box.const_get(name, false)]
|
|
184
|
-
end
|
|
185
|
-
[@public_method_entries, constants]
|
|
176
|
+
@export
|
|
186
177
|
end
|
|
187
178
|
|
|
188
179
|
def reset_export
|
|
189
180
|
@export = NO_EXPORT
|
|
190
181
|
end
|
|
191
|
-
|
|
192
|
-
def snapshot_import_support
|
|
193
|
-
@import_constant_names = @box.constants(false).map(&:to_s)
|
|
194
|
-
@import_method_names = top_level_methods.keys
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def discover_public_entries
|
|
198
|
-
@public_constant_names =
|
|
199
|
-
@box.constants(false).map(&:to_s) - @import_constant_names
|
|
200
|
-
@public_method_entries = top_level_methods.except(*@import_method_names)
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def top_level_methods
|
|
204
|
-
@box.eval(
|
|
205
|
-
'Object.private_instance_methods(false).to_h { |name| [name.to_s, method(name)] }',
|
|
206
|
-
)
|
|
207
|
-
end
|
|
208
182
|
end
|
|
209
183
|
end
|
data/lib/carton/bundler.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Carton
|
|
4
4
|
class << self
|
|
5
|
-
# Set up the current
|
|
5
|
+
# Set up the current Carton's bundle, like `Bundler.setup(*groups)`.
|
|
6
6
|
#
|
|
7
7
|
# Bundler finds its Gemfile through process-global `ENV["BUNDLE_GEMFILE"]`
|
|
8
8
|
# or the real process cwd. Carton resolves the Gemfile from the calling
|
data/lib/carton/error.rb
CHANGED
|
@@ -8,13 +8,12 @@ module Carton
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
# An Import that cannot go ahead: Boxes are disabled, a Carton imports
|
|
11
|
-
# itself, or a
|
|
11
|
+
# itself, or a shared Carton was built against other gem versions.
|
|
12
12
|
class ImportError < Error
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
# An
|
|
16
|
-
#
|
|
17
|
-
# method.
|
|
15
|
+
# An Entrypoint finished without exporting, called `export` twice, or
|
|
16
|
+
# gave it a Symbol.
|
|
18
17
|
class ExportError < Error
|
|
19
18
|
end
|
|
20
19
|
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Carton
|
|
4
|
-
# Carries the
|
|
4
|
+
# Carries the Importer's activated gems into a Carton Box.
|
|
5
5
|
#
|
|
6
6
|
# Optional Boxes copy Master, so they start with clean RubyGems state and
|
|
7
7
|
# would auto-activate the newest installed version of any dependency. When
|
|
8
8
|
# an Import resolves inside an activated gem, Carton activates that gem and
|
|
9
|
-
# its runtime dependencies in the new Box at the
|
|
10
|
-
#
|
|
9
|
+
# its runtime dependencies in the new Box at the Importer's versions, so the
|
|
10
|
+
# Importer's lockfile (or plain `gem` calls) decides what the Carton sees.
|
|
11
11
|
#
|
|
12
|
-
# When the
|
|
13
|
-
#
|
|
12
|
+
# When the Importer ran Bundler setup, the Box is also restricted to the
|
|
13
|
+
# Importer's locked gems, as `Bundler.setup` restricts Main: every locked gem
|
|
14
14
|
# is activated at its locked version and anything else fails to load.
|
|
15
15
|
module GemActivation
|
|
16
16
|
module_function
|
|
17
17
|
|
|
18
|
-
# The gem in `loaded_specs` that owns
|
|
19
|
-
# runtime dependencies, dependencies first. Default
|
|
20
|
-
# would otherwise load the newest installed version,
|
|
18
|
+
# The Gem Closure of `feature`: the gem in `loaded_specs` that owns it,
|
|
19
|
+
# plus its activated runtime dependencies, dependencies first. Default
|
|
20
|
+
# gems count: a new Box would otherwise load the newest installed version,
|
|
21
|
+
# not the Importer's.
|
|
21
22
|
# Empty when no activated gem owns the file.
|
|
22
23
|
def closure(feature, loaded_specs)
|
|
23
24
|
owner = owner_of(feature, loaded_specs.values)
|
|
@@ -26,7 +27,7 @@ module Carton
|
|
|
26
27
|
visit(owner, loaded_specs, [], {})
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
# The
|
|
30
|
+
# The Importer's locked gems when `box` ran Bundler setup, otherwise nil.
|
|
30
31
|
# Bundler marks every locked spec as loaded, so these are the shareable
|
|
31
32
|
# entries of its `Gem.loaded_specs`.
|
|
32
33
|
def locked(box, loaded_specs)
|
|
@@ -57,7 +58,7 @@ module Carton
|
|
|
57
58
|
#
|
|
58
59
|
# Default gems come first, from this Box's own default specs: the YAML
|
|
59
60
|
# round trip loads Psych, whose requires (date, for one) would otherwise
|
|
60
|
-
# activate the newest installed version before the
|
|
61
|
+
# activate the newest installed version before the Importer's.
|
|
61
62
|
def activate(specs, locked = nil)
|
|
62
63
|
defaults, others = (locked || specs).partition(&:default_gem?)
|
|
63
64
|
defaults.each { |source| activate_spec(default_spec(source)) }
|
|
@@ -138,8 +139,8 @@ module Carton
|
|
|
138
139
|
private_class_method :visit
|
|
139
140
|
|
|
140
141
|
# Bundler is never shared: each Box loads its own copy to apply the
|
|
141
|
-
# restriction, possibly a different version than the
|
|
142
|
-
# activating the
|
|
142
|
+
# restriction, possibly a different version than the Importer's, and
|
|
143
|
+
# activating the Importer's on top of it raises Gem::LoadError.
|
|
143
144
|
def activatable?(spec) = spec.name != 'bundler'
|
|
144
145
|
private_class_method :activatable?
|
|
145
146
|
|
data/lib/carton/import_cache.rb
CHANGED
|
@@ -6,9 +6,7 @@ module Carton
|
|
|
6
6
|
# The Main Box creates one instance and every Carton Box receives that same
|
|
7
7
|
# object, so each Entrypoint loads once no matter which Carton imports it.
|
|
8
8
|
# Ruby objects are shared across Boxes; only definitions are Box-local, so
|
|
9
|
-
# the Main Box's loader builds every Carton
|
|
10
|
-
# value or `Carton::Exports` from the Main Box regardless of which
|
|
11
|
-
# Carton imported it first.
|
|
9
|
+
# the Main Box's loader builds every Carton and caches its Export.
|
|
12
10
|
class ImportCache
|
|
13
11
|
# Returned by `claim` when the calling thread should load the Entrypoint.
|
|
14
12
|
LOAD = Object.new.freeze
|
|
@@ -80,9 +78,9 @@ module Carton
|
|
|
80
78
|
end
|
|
81
79
|
end
|
|
82
80
|
|
|
83
|
-
# A
|
|
84
|
-
#
|
|
85
|
-
# since. Raise instead of silently sharing it with an
|
|
81
|
+
# A shared Carton keeps the gem versions its First Importer gave it: its
|
|
82
|
+
# Gem Closure and the locked gems it has loaded, including any loaded lazily
|
|
83
|
+
# since. Raise instead of silently sharing it with an Importer that
|
|
86
84
|
# activates or locks another version of one.
|
|
87
85
|
def check_gem_versions(key, target)
|
|
88
86
|
wanted = versions([*target.locked_gems, *target.gems])
|
data/lib/carton/kernel_patch.rb
CHANGED
|
@@ -4,19 +4,19 @@ module Carton
|
|
|
4
4
|
module KernelPatch
|
|
5
5
|
private # like `require` and `require_relative`
|
|
6
6
|
|
|
7
|
-
# Import a
|
|
7
|
+
# Import a Carton by absolute path, or by feature name from the Importer's
|
|
8
8
|
# current `$LOAD_PATH`.
|
|
9
9
|
def import(path)
|
|
10
10
|
Runtime.import(path, from: caller_locations(1, 1).first.path)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
# Import a
|
|
13
|
+
# Import a Carton relative to the calling file, like `require_relative`.
|
|
14
14
|
def import_relative(path)
|
|
15
15
|
caller_dir = File.dirname(caller_locations(1, 1).first.path)
|
|
16
16
|
Runtime.import(path, base_dir: caller_dir)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# Export one value from the current
|
|
19
|
+
# Export one value from the current Carton; `import` returns it as is.
|
|
20
20
|
# Keywords export a Hash, as in `export version: '1.0'`.
|
|
21
21
|
def export(value)
|
|
22
22
|
file = caller_locations(1, 1).first.absolute_path
|
data/lib/carton/resolver.rb
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Carton
|
|
4
4
|
# Maps an Import path to the file that should load, without evaluating any
|
|
5
|
-
# code or reading Box state. The runtime passes the
|
|
5
|
+
# code or reading Box state. The runtime passes the Importer's
|
|
6
6
|
# `$LOAD_PATH`; a static analyzer can pass any load path it has computed.
|
|
7
7
|
module Resolver
|
|
8
|
-
# `feature` is what the new Box requires. `load_path` is the single
|
|
8
|
+
# `feature` is what the new Box requires. `load_path` is the single Importer
|
|
9
9
|
# load-path entry that matched a name-based Import, if any. The runtime
|
|
10
|
-
# fills `gems` with the
|
|
11
|
-
# `locked_gems` with the
|
|
10
|
+
# fills `gems` with the Gem Closure to activate in the new Box, and
|
|
11
|
+
# `locked_gems` with the Importer's locked specs when it ran Bundler setup.
|
|
12
12
|
# After loading, the Carton Box sets `gem_usage`, a callable returning the
|
|
13
|
-
# gems the Carton depends on at that moment: its
|
|
14
|
-
# gem whose files it has loaded so far.
|
|
13
|
+
# gems the Carton depends on at that moment: its Gem Closure plus every
|
|
14
|
+
# locked gem whose files it has loaded so far.
|
|
15
15
|
Target =
|
|
16
16
|
Struct.new(
|
|
17
17
|
:feature,
|
data/lib/carton/runtime.rb
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Carton
|
|
4
4
|
module Runtime
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
# Carton's own library, which every Carton Box loads so its Entrypoint can
|
|
6
|
+
# call `import` and `export`. It is not an Entrypoint.
|
|
7
|
+
LIBRARY = File.expand_path('../carton.rb', __dir__)
|
|
8
|
+
private_constant :LIBRARY
|
|
7
9
|
# Used only when this Box is the Main Box. Carton Boxes reuse the Main
|
|
8
10
|
# Box cache, so every Carton is built by the Main Box loader.
|
|
9
11
|
IMPORT_CACHE = ImportCache.new { |target| load_import(target) }
|
|
@@ -11,24 +13,22 @@ module Carton
|
|
|
11
13
|
|
|
12
14
|
module_function
|
|
13
15
|
|
|
14
|
-
# Load the target
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# `
|
|
18
|
-
# caller Box's `$LOAD_PATH`, and `from` names the calling file for its
|
|
19
|
-
# error message.
|
|
16
|
+
# Load the target in a new Carton Box and return its Export. Each Entrypoint
|
|
17
|
+
# loads once per process; later imports return the same value. `base_dir` is
|
|
18
|
+
# given for `import_relative`; plain `import` searches the Importer's
|
|
19
|
+
# `$LOAD_PATH`, and `from` names the calling file for its error message.
|
|
20
20
|
def import(path, base_dir: nil, from: nil)
|
|
21
21
|
unless Ruby::Box.enabled?
|
|
22
22
|
raise ImportError, 'Carton needs Ruby::Box: run Ruby with RUBY_BOX=1'
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
load_path =
|
|
25
|
+
importer_box = Ruby::Box.current
|
|
26
|
+
load_path = importer_box.eval('$LOAD_PATH.to_a')
|
|
27
27
|
target = Resolver.resolve(path, load_path:, base_dir:)
|
|
28
|
-
loaded_specs =
|
|
28
|
+
loaded_specs = importer_box.eval('defined?(Gem) ? Gem.loaded_specs : {}')
|
|
29
29
|
target.gems = GemActivation.closure(target.feature, loaded_specs)
|
|
30
30
|
unless target.gems.empty?
|
|
31
|
-
target.locked_gems = GemActivation.locked(
|
|
31
|
+
target.locked_gems = GemActivation.locked(importer_box, loaded_specs)
|
|
32
32
|
end
|
|
33
33
|
begin
|
|
34
34
|
import_cache.import(target)
|
|
@@ -39,7 +39,7 @@ module Carton
|
|
|
39
39
|
end
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
# Make `value` the current
|
|
42
|
+
# Make `value` the current Carton's Export, which `import` returns as
|
|
43
43
|
# is. A Symbol is refused even outside the Entrypoint: `export def name`
|
|
44
44
|
# and `export :Name` would export the Symbol itself.
|
|
45
45
|
def export(value, file:)
|
|
@@ -54,7 +54,7 @@ module Carton
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
# A name not on `$LOAD_PATH` is the usual mistake of `import` for a file
|
|
57
|
-
# beside the
|
|
57
|
+
# beside the calling file, so point at `import_relative` when one is there.
|
|
58
58
|
def missing_name_message(path, message, from)
|
|
59
59
|
message = "#{message}: import searches $LOAD_PATH, like require"
|
|
60
60
|
dir = from && File.dirname(File.expand_path(from))
|
|
@@ -68,14 +68,12 @@ module Carton
|
|
|
68
68
|
private_class_method :missing_name_message
|
|
69
69
|
|
|
70
70
|
def load_import(target)
|
|
71
|
-
Box
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
import_cache: IMPORT_CACHE,
|
|
78
|
-
) { |methods, constants| Namespace.new(methods, constants) }
|
|
71
|
+
Box.new.__send__(
|
|
72
|
+
:import_target,
|
|
73
|
+
target,
|
|
74
|
+
library: LIBRARY,
|
|
75
|
+
import_cache: IMPORT_CACHE,
|
|
76
|
+
)
|
|
79
77
|
end
|
|
80
78
|
private_class_method :load_import
|
|
81
79
|
|
data/lib/carton/version.rb
CHANGED
data/lib/carton.rb
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'carton/error'
|
|
4
4
|
require_relative 'carton/bundler'
|
|
5
|
-
require_relative 'carton/namespace_methods'
|
|
6
|
-
require_relative 'carton/namespace'
|
|
7
5
|
require_relative 'carton/box'
|
|
8
6
|
require_relative 'carton/gem_activation'
|
|
9
7
|
require_relative 'carton/import_cache'
|
|
@@ -16,7 +14,6 @@ module Carton
|
|
|
16
14
|
private_constant :GemActivation
|
|
17
15
|
private_constant :ImportCache
|
|
18
16
|
private_constant :KernelPatch
|
|
19
|
-
private_constant :NamespaceMethods
|
|
20
17
|
private_constant :Resolver
|
|
21
18
|
private_constant :Runtime
|
|
22
19
|
end
|
data/sig/carton.rbs
CHANGED
|
@@ -23,27 +23,3 @@ end
|
|
|
23
23
|
|
|
24
24
|
class Carton::GemfileNotFound < Carton::Error
|
|
25
25
|
end
|
|
26
|
-
|
|
27
|
-
# Lookup helpers on every `Carton::Namespace`. `carton rbs` extends its
|
|
28
|
-
# generated `Carton::Namespace::*` modules with these.
|
|
29
|
-
module Carton::NamespaceMethods
|
|
30
|
-
type key = ::String | ::Symbol
|
|
31
|
-
|
|
32
|
-
# Helpers come back as Method objects; constants as set.
|
|
33
|
-
def []: (key key) -> untyped
|
|
34
|
-
def deconstruct_keys: (nil keys) -> ::Hash[::Symbol, untyped]
|
|
35
|
-
| (::Array[::Symbol] keys) -> ::Hash[::Symbol, untyped]
|
|
36
|
-
def fetch: (key key) -> untyped
|
|
37
|
-
| (key key, untyped default) -> untyped
|
|
38
|
-
| [T] (key key) { (key key) -> T } -> (untyped | T)
|
|
39
|
-
def fetch_values: (*key keys) -> ::Array[untyped]
|
|
40
|
-
def keys: () -> ::Array[::Symbol]
|
|
41
|
-
def to_h: () -> ::Hash[::Symbol, untyped]
|
|
42
|
-
def has_key?: (key key) -> bool
|
|
43
|
-
def key?: (key key) -> bool
|
|
44
|
-
def values_at: (*key keys) -> ::Array[untyped]
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
class Carton::Namespace < ::Module
|
|
48
|
-
include ::Carton::NamespaceMethods
|
|
49
|
-
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: carton
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cristofaro
|
|
@@ -9,10 +9,11 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
can use different versions of the same gem. No manifest
|
|
12
|
+
description: Split your app into Cartons with explicit imports and exports. Each has
|
|
13
|
+
its own dependencies and runs in a separate Ruby::Box, so Ruby enforces the boundaries
|
|
14
|
+
at runtime. Only what a Carton exports is reachable, monkey patches stay where they
|
|
15
|
+
are made, and two Cartons can use different versions of the same gem. No manifest
|
|
16
|
+
needed. Requires Ruby 4.0.7+.
|
|
16
17
|
email:
|
|
17
18
|
- david@dtcristo.com
|
|
18
19
|
executables: []
|
|
@@ -31,8 +32,6 @@ files:
|
|
|
31
32
|
- lib/carton/import_cache.rb
|
|
32
33
|
- lib/carton/kernel_patch.rb
|
|
33
34
|
- lib/carton/loaded_features.rb
|
|
34
|
-
- lib/carton/namespace.rb
|
|
35
|
-
- lib/carton/namespace_methods.rb
|
|
36
35
|
- lib/carton/resolver.rb
|
|
37
36
|
- lib/carton/runtime.rb
|
|
38
37
|
- lib/carton/version.rb
|
data/lib/carton/namespace.rb
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Carton
|
|
4
|
-
# What importing a Bare Carton returns: a module of the constants and
|
|
5
|
-
# top-level helpers its Entrypoint defined. Helpers become singleton
|
|
6
|
-
# methods that forward to the Carton's method, and lookup helpers return
|
|
7
|
-
# the `Method` object or the constant.
|
|
8
|
-
class Namespace < ::Module
|
|
9
|
-
include NamespaceMethods
|
|
10
|
-
|
|
11
|
-
# A helper becomes a singleton method, so one named like a method every
|
|
12
|
-
# `Carton::Namespace` has, such as `fetch` or `name`, would hide it.
|
|
13
|
-
# Constants are capitalized and never collide.
|
|
14
|
-
def self.reserved?(name)
|
|
15
|
-
name = name.to_s
|
|
16
|
-
!name.match?(/\A[A-Z]/) && RESERVED.include?(name)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Raise for the first reserved name in `names`.
|
|
20
|
-
def self.check_names(names)
|
|
21
|
-
name = names.find { |n| reserved?(n) } or return
|
|
22
|
-
|
|
23
|
-
raise ExportError,
|
|
24
|
-
"a top-level method named #{name} would hide " \
|
|
25
|
-
"Carton::Namespace##{name}: rename it, or export a module"
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def initialize(methods = {}, constants = {})
|
|
29
|
-
super()
|
|
30
|
-
Namespace.check_names(methods.keys)
|
|
31
|
-
@lookup = {}
|
|
32
|
-
define_methods(methods)
|
|
33
|
-
define_constants(constants)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def deconstruct_keys(keys)
|
|
37
|
-
return super if keys
|
|
38
|
-
|
|
39
|
-
to_h
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# `#<Carton::Namespace PI, add>`: its names, so a namespace is
|
|
43
|
-
# recognizable in a console.
|
|
44
|
-
def inspect
|
|
45
|
-
names = @lookup.keys.join(', ')
|
|
46
|
-
names.empty? ? '#<Carton::Namespace>' : "#<Carton::Namespace #{names}>"
|
|
47
|
-
end
|
|
48
|
-
alias to_s inspect
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def define_methods(methods)
|
|
53
|
-
methods.each do |name, method|
|
|
54
|
-
@lookup[name.to_s] = method
|
|
55
|
-
define_singleton_method(name) do |*args, **kwargs, &block|
|
|
56
|
-
method.call(*args, **kwargs, &block)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def define_constants(constants)
|
|
62
|
-
constants.each do |name, value|
|
|
63
|
-
@lookup[name.to_s] = value
|
|
64
|
-
const_set(name, value)
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def lookup_entry(key)
|
|
69
|
-
name = key.to_s
|
|
70
|
-
@lookup.key?(name) ? [true, @lookup[name]] : [false, nil]
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def lookup_names = @lookup.keys
|
|
74
|
-
|
|
75
|
-
RESERVED = public_instance_methods.map(&:to_s).freeze
|
|
76
|
-
private_constant :RESERVED
|
|
77
|
-
end
|
|
78
|
-
end
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Carton
|
|
4
|
-
# Hash-like lookup helpers for `Carton::Namespace`.
|
|
5
|
-
module NamespaceMethods
|
|
6
|
-
# Support destructuring like `import('math') => { add: }`.
|
|
7
|
-
def deconstruct_keys(keys)
|
|
8
|
-
keys.each_with_object({}) do |key, hash|
|
|
9
|
-
found, value = lookup_entry(key)
|
|
10
|
-
hash[key] = value if found
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# Return the member or `nil`.
|
|
15
|
-
def [](key)
|
|
16
|
-
found, value = lookup_entry(key)
|
|
17
|
-
found ? value : nil
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Check whether the lookup will succeed.
|
|
21
|
-
def key?(key)
|
|
22
|
-
lookup_entry(key).first
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
alias has_key? key?
|
|
26
|
-
|
|
27
|
-
# Fetch a member with the same default/block semantics as `Hash#fetch`.
|
|
28
|
-
def fetch(key, *default)
|
|
29
|
-
if default.size > 1
|
|
30
|
-
raise ArgumentError,
|
|
31
|
-
"wrong number of arguments (given #{default.size + 1}, expected 1..2)"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
found, value = lookup_entry(key)
|
|
35
|
-
return value if found
|
|
36
|
-
return yield key if block_given?
|
|
37
|
-
return default.first unless default.empty?
|
|
38
|
-
|
|
39
|
-
raise KeyError, "key not found: #{key.inspect}"
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Member names as Symbols: helpers first, then constants.
|
|
43
|
-
def keys = lookup_names.map(&:to_sym)
|
|
44
|
-
|
|
45
|
-
# Every member by name: helpers as `Method` objects, constants as set.
|
|
46
|
-
def to_h = keys.to_h { |key| [key, self[key]] }
|
|
47
|
-
|
|
48
|
-
# Fetch several members and raise if any are missing.
|
|
49
|
-
def fetch_values(*keys)
|
|
50
|
-
keys.map { |key| fetch(key) }
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Return several members, using `nil` for missing keys.
|
|
54
|
-
def values_at(*keys)
|
|
55
|
-
keys.map { |key| self[key] }
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|