boxwerk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +24 -0
- data/ARCHITECTURE.md +264 -0
- data/CHANGELOG.md +59 -11
- data/README.md +56 -174
- data/Rakefile +46 -3
- data/TODO.md +317 -0
- data/USAGE.md +505 -0
- data/exe/boxwerk +55 -14
- data/lib/boxwerk/autoloader_mixin.rb +65 -0
- data/lib/boxwerk/box_manager.rb +405 -0
- data/lib/boxwerk/cli.rb +776 -37
- data/lib/boxwerk/constant_resolver.rb +236 -0
- data/lib/boxwerk/gem_resolver.rb +235 -0
- data/lib/boxwerk/gemfile_require_parser.rb +50 -0
- data/lib/boxwerk/global_context.rb +85 -0
- data/lib/boxwerk/package.rb +76 -24
- data/lib/boxwerk/package_context.rb +103 -0
- data/lib/boxwerk/package_resolver.rb +122 -0
- data/lib/boxwerk/privacy_checker.rb +159 -0
- data/lib/boxwerk/setup.rb +124 -16
- data/lib/boxwerk/version.rb +1 -1
- data/lib/boxwerk/zeitwerk_scanner.rb +172 -0
- data/lib/boxwerk.rb +30 -3
- metadata +54 -11
- data/lib/boxwerk/graph.rb +0 -53
- data/lib/boxwerk/loader.rb +0 -149
- data/lib/boxwerk/registry.rb +0 -26
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'zeitwerk'
|
|
4
|
+
|
|
5
|
+
module Boxwerk
|
|
6
|
+
# Uses Zeitwerk's file system scanner and inflector to discover constants
|
|
7
|
+
# in a directory. Zeitwerk's autoload registration cannot be used directly
|
|
8
|
+
# inside Ruby::Box (autoloads register in the box where the code was
|
|
9
|
+
# defined, not where it's called), so we only use Zeitwerk for:
|
|
10
|
+
#
|
|
11
|
+
# - File discovery (respecting Zeitwerk conventions: hidden dirs, etc.)
|
|
12
|
+
# - Inflection (file names → constant names)
|
|
13
|
+
#
|
|
14
|
+
# The actual autoload registration is done by BoxManager using box.eval.
|
|
15
|
+
module ZeitwerkScanner
|
|
16
|
+
Entry = Data.define(:type, :cname, :full_path, :file, :parent, :dir)
|
|
17
|
+
|
|
18
|
+
# Scans a directory and returns an array of Entry structs describing
|
|
19
|
+
# the constants and namespaces found. Uses a temporary Zeitwerk::Loader
|
|
20
|
+
# for its FileSystem scanner and Inflector.
|
|
21
|
+
def self.scan(dir)
|
|
22
|
+
loader = Zeitwerk::Loader.new
|
|
23
|
+
loader.push_dir(dir)
|
|
24
|
+
inflector = loader.inflector
|
|
25
|
+
fs = Zeitwerk::Loader::FileSystem.new(loader)
|
|
26
|
+
|
|
27
|
+
entries = []
|
|
28
|
+
scan_dir(fs, inflector, dir, '', entries)
|
|
29
|
+
entries
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Registers autoloads in a Ruby::Box based on scan results.
|
|
33
|
+
# Implicit namespaces become Module.new, explicit namespaces are
|
|
34
|
+
# eagerly loaded so child autoloads can attach to them.
|
|
35
|
+
def self.register_autoloads(box, entries)
|
|
36
|
+
namespaces = entries.select { |e| e.type == :namespace }
|
|
37
|
+
files = entries.select { |e| e.type == :file }
|
|
38
|
+
|
|
39
|
+
# Deduplicate namespaces: when both lib/ and public/ contribute a
|
|
40
|
+
# namespace with the same full_path, prefer the explicit one (has a
|
|
41
|
+
# .rb file) so the module definition is loaded rather than replaced
|
|
42
|
+
# by an empty Module.new.
|
|
43
|
+
namespaces =
|
|
44
|
+
namespaces
|
|
45
|
+
.group_by(&:full_path)
|
|
46
|
+
.values
|
|
47
|
+
.map { |group| group.find { |ns| ns.file } || group.first }
|
|
48
|
+
|
|
49
|
+
# Phase 1: Set up namespaces
|
|
50
|
+
namespaces.each do |ns|
|
|
51
|
+
if ns.file
|
|
52
|
+
# Explicit namespace: autoload the .rb file
|
|
53
|
+
register_autoload(box, ns.parent, ns.cname, ns.file)
|
|
54
|
+
else
|
|
55
|
+
# Implicit namespace: create empty module
|
|
56
|
+
define_implicit_module(box, ns.parent, ns.cname, ns.full_path)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Phase 2: Eagerly trigger explicit namespaces so children can attach
|
|
61
|
+
namespaces.each { |ns| box.eval(ns.full_path) if ns.file }
|
|
62
|
+
|
|
63
|
+
# Phase 3: Register file autoloads
|
|
64
|
+
files.each { |f| register_autoload(box, f.parent, f.cname, f.file) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Scans files in a collapsed directory, computing the parent namespace
|
|
68
|
+
# from the path between root_dir and the collapsed dir. E.g. if root_dir
|
|
69
|
+
# is lib/ and dir is lib/analytics/formatters/, files map to Analytics::*.
|
|
70
|
+
def self.scan_files_only(dir, root_dir: nil)
|
|
71
|
+
inflector = Zeitwerk::Inflector.new
|
|
72
|
+
entries = []
|
|
73
|
+
|
|
74
|
+
parent_cnames =
|
|
75
|
+
if root_dir && dir.start_with?("#{root_dir}/")
|
|
76
|
+
rel = dir.delete_prefix("#{root_dir}/")
|
|
77
|
+
rel.split('/')[0...-1].map { |p| inflector.camelize(p, root_dir) }
|
|
78
|
+
else
|
|
79
|
+
[]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
Dir
|
|
83
|
+
.glob(File.join(dir, '**', '*.rb'))
|
|
84
|
+
.sort
|
|
85
|
+
.each do |abspath|
|
|
86
|
+
relative = abspath.delete_prefix("#{dir}/").delete_suffix('.rb')
|
|
87
|
+
parts = relative.split('/')
|
|
88
|
+
file_cnames = parts.map { |part| inflector.camelize(part, dir) }
|
|
89
|
+
all_cnames = parent_cnames + file_cnames
|
|
90
|
+
full_path = all_cnames.join('::')
|
|
91
|
+
cname = all_cnames.last
|
|
92
|
+
parent = all_cnames[0...-1].join('::')
|
|
93
|
+
|
|
94
|
+
entries << Entry.new(
|
|
95
|
+
type: :file,
|
|
96
|
+
cname: cname,
|
|
97
|
+
full_path: full_path,
|
|
98
|
+
file: abspath,
|
|
99
|
+
parent: parent,
|
|
100
|
+
dir: nil,
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
entries
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Builds a file index (const_name → file_path) from scan entries.
|
|
108
|
+
# Used by ConstantResolver for dependency wiring.
|
|
109
|
+
def self.build_file_index(entries)
|
|
110
|
+
index = {}
|
|
111
|
+
entries.each { |e| index[e.full_path] = e.file if e.file }
|
|
112
|
+
index
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class << self
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def scan_dir(fs, inflector, dir, parent_path, entries)
|
|
119
|
+
fs.ls(dir) do |basename, abspath, ftype|
|
|
120
|
+
if ftype == :file
|
|
121
|
+
# Skip files that have a matching directory (explicit namespaces).
|
|
122
|
+
# These are already handled as namespace entries with their .rb file.
|
|
123
|
+
next if File.directory?(abspath.delete_suffix('.rb'))
|
|
124
|
+
|
|
125
|
+
cname = inflector.camelize(basename.delete_suffix('.rb'), dir)
|
|
126
|
+
full_path = parent_path.empty? ? cname : "#{parent_path}::#{cname}"
|
|
127
|
+
entries << Entry.new(
|
|
128
|
+
type: :file,
|
|
129
|
+
cname: cname,
|
|
130
|
+
full_path: full_path,
|
|
131
|
+
file: abspath,
|
|
132
|
+
parent: parent_path,
|
|
133
|
+
dir: nil,
|
|
134
|
+
)
|
|
135
|
+
elsif ftype == :directory
|
|
136
|
+
cname = inflector.camelize(basename, dir)
|
|
137
|
+
full_path = parent_path.empty? ? cname : "#{parent_path}::#{cname}"
|
|
138
|
+
rb_file = "#{abspath}.rb"
|
|
139
|
+
has_rb = File.exist?(rb_file)
|
|
140
|
+
entries << Entry.new(
|
|
141
|
+
type: :namespace,
|
|
142
|
+
cname: cname,
|
|
143
|
+
full_path: full_path,
|
|
144
|
+
file: has_rb ? rb_file : nil,
|
|
145
|
+
parent: parent_path,
|
|
146
|
+
dir: abspath,
|
|
147
|
+
)
|
|
148
|
+
scan_dir(fs, inflector, abspath, full_path, entries)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def register_autoload(box, parent, cname, file)
|
|
154
|
+
if parent.empty?
|
|
155
|
+
box.eval("autoload :#{cname}, #{file.inspect}")
|
|
156
|
+
else
|
|
157
|
+
box.eval("#{parent}.autoload(:#{cname}, #{file.inspect})")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def define_implicit_module(box, parent, cname, full_path)
|
|
162
|
+
if parent.empty?
|
|
163
|
+
box.eval("#{cname} = Module.new unless defined?(#{cname})")
|
|
164
|
+
else
|
|
165
|
+
box.eval(
|
|
166
|
+
"#{parent}.const_set(:#{cname}, Module.new) unless defined?(#{full_path})",
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
data/lib/boxwerk.rb
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'boxwerk/autoloader_mixin'
|
|
4
|
+
require_relative 'boxwerk/box_manager'
|
|
3
5
|
require_relative 'boxwerk/cli'
|
|
4
|
-
require_relative 'boxwerk/
|
|
5
|
-
require_relative 'boxwerk/
|
|
6
|
+
require_relative 'boxwerk/constant_resolver'
|
|
7
|
+
require_relative 'boxwerk/gem_resolver'
|
|
8
|
+
require_relative 'boxwerk/gemfile_require_parser'
|
|
9
|
+
require_relative 'boxwerk/global_context'
|
|
6
10
|
require_relative 'boxwerk/package'
|
|
7
|
-
require_relative 'boxwerk/
|
|
11
|
+
require_relative 'boxwerk/package_context'
|
|
12
|
+
require_relative 'boxwerk/package_resolver'
|
|
13
|
+
require_relative 'boxwerk/privacy_checker'
|
|
8
14
|
require_relative 'boxwerk/setup'
|
|
9
15
|
require_relative 'boxwerk/version'
|
|
16
|
+
require_relative 'boxwerk/zeitwerk_scanner'
|
|
17
|
+
|
|
18
|
+
# Boxwerk is a package isolation system for Ruby applications built on
|
|
19
|
+
# Ruby::Box. It loads each package in its own +Ruby::Box+, enforcing
|
|
20
|
+
# dependency and privacy boundaries declared in +package.yml+ files.
|
|
21
|
+
module Boxwerk
|
|
22
|
+
class << self
|
|
23
|
+
# Returns the {PackageContext} for the currently executing package.
|
|
24
|
+
# Returns +nil+ in the root box.
|
|
25
|
+
# @return [PackageContext, nil]
|
|
26
|
+
def package
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the {GlobalContext}.
|
|
31
|
+
# @return [GlobalContext]
|
|
32
|
+
def global
|
|
33
|
+
Ruby::Box.root.const_get(:BOXWERK_GLOBAL)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: boxwerk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Cristofaro
|
|
@@ -9,23 +9,54 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: bundler
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '4.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '4.0'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: irb
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
15
29
|
requirements:
|
|
16
30
|
- - "~>"
|
|
17
31
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '1.
|
|
32
|
+
version: '1.17'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.17'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: zeitwerk
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.7'
|
|
19
47
|
type: :runtime
|
|
20
48
|
prerelease: false
|
|
21
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
50
|
requirements:
|
|
23
51
|
- - "~>"
|
|
24
52
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
26
|
-
description: Boxwerk is
|
|
27
|
-
|
|
28
|
-
|
|
53
|
+
version: '2.7'
|
|
54
|
+
description: Boxwerk is a tool for creating modular Ruby and Rails applications. It
|
|
55
|
+
organizes code into packages with clear boundaries and explicit dependencies, enforcing
|
|
56
|
+
them at runtime using Ruby::Box constant isolation. It reads standard Packwerk package.yml
|
|
57
|
+
files (without requiring Packwerk), providing per-package gem isolation, Zeitwerk-based
|
|
58
|
+
autoloading, monkey patch isolation between packages, and a CLI for running, testing,
|
|
59
|
+
and inspecting your modular application.
|
|
29
60
|
email:
|
|
30
61
|
- david@dtcristo.com
|
|
31
62
|
executables:
|
|
@@ -33,19 +64,30 @@ executables:
|
|
|
33
64
|
extensions: []
|
|
34
65
|
extra_rdoc_files: []
|
|
35
66
|
files:
|
|
67
|
+
- AGENTS.md
|
|
68
|
+
- ARCHITECTURE.md
|
|
36
69
|
- CHANGELOG.md
|
|
37
70
|
- LICENSE.txt
|
|
38
71
|
- README.md
|
|
39
72
|
- Rakefile
|
|
73
|
+
- TODO.md
|
|
74
|
+
- USAGE.md
|
|
40
75
|
- exe/boxwerk
|
|
41
76
|
- lib/boxwerk.rb
|
|
77
|
+
- lib/boxwerk/autoloader_mixin.rb
|
|
78
|
+
- lib/boxwerk/box_manager.rb
|
|
42
79
|
- lib/boxwerk/cli.rb
|
|
43
|
-
- lib/boxwerk/
|
|
44
|
-
- lib/boxwerk/
|
|
80
|
+
- lib/boxwerk/constant_resolver.rb
|
|
81
|
+
- lib/boxwerk/gem_resolver.rb
|
|
82
|
+
- lib/boxwerk/gemfile_require_parser.rb
|
|
83
|
+
- lib/boxwerk/global_context.rb
|
|
45
84
|
- lib/boxwerk/package.rb
|
|
46
|
-
- lib/boxwerk/
|
|
85
|
+
- lib/boxwerk/package_context.rb
|
|
86
|
+
- lib/boxwerk/package_resolver.rb
|
|
87
|
+
- lib/boxwerk/privacy_checker.rb
|
|
47
88
|
- lib/boxwerk/setup.rb
|
|
48
89
|
- lib/boxwerk/version.rb
|
|
90
|
+
- lib/boxwerk/zeitwerk_scanner.rb
|
|
49
91
|
homepage: https://github.com/dtcristo/boxwerk
|
|
50
92
|
licenses:
|
|
51
93
|
- MIT
|
|
@@ -54,6 +96,7 @@ metadata:
|
|
|
54
96
|
homepage_uri: https://github.com/dtcristo/boxwerk
|
|
55
97
|
source_code_uri: https://github.com/dtcristo/boxwerk
|
|
56
98
|
changelog_uri: https://github.com/dtcristo/boxwerk/blob/main/CHANGELOG.md
|
|
99
|
+
documentation_uri: https://dtcristo.github.io/boxwerk/
|
|
57
100
|
rdoc_options: []
|
|
58
101
|
require_paths:
|
|
59
102
|
- lib
|
|
@@ -61,7 +104,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
61
104
|
requirements:
|
|
62
105
|
- - ">="
|
|
63
106
|
- !ruby/object:Gem::Version
|
|
64
|
-
version: 4.0
|
|
107
|
+
version: '4.0'
|
|
65
108
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
109
|
requirements:
|
|
67
110
|
- - ">="
|
|
@@ -70,5 +113,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
70
113
|
requirements: []
|
|
71
114
|
rubygems_version: 4.0.3
|
|
72
115
|
specification_version: 4
|
|
73
|
-
summary: Ruby package system with Box-powered
|
|
116
|
+
summary: Ruby package system with Box-powered boundary enforcement
|
|
74
117
|
test_files: []
|
data/lib/boxwerk/graph.rb
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Boxwerk
|
|
4
|
-
# Graph builds a directed acyclic graph (DAG) of package dependencies.
|
|
5
|
-
# Validates no circular dependencies and provides topological ordering for boot sequence.
|
|
6
|
-
class Graph
|
|
7
|
-
attr_reader :packages, :root
|
|
8
|
-
|
|
9
|
-
def initialize(root_path)
|
|
10
|
-
@root_path = root_path
|
|
11
|
-
@packages = {}
|
|
12
|
-
@root = load_package('root', root_path)
|
|
13
|
-
resolve_dependencies(@root, [])
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def topological_order
|
|
17
|
-
visited = {}
|
|
18
|
-
order = []
|
|
19
|
-
@packages.each_value { |pkg| visit(pkg, visited, order, []) }
|
|
20
|
-
order
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def load_package(name, path)
|
|
26
|
-
return @packages[name] if @packages[name]
|
|
27
|
-
|
|
28
|
-
@packages[name] = Package.new(name, path)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def resolve_dependencies(package, path)
|
|
32
|
-
raise "Circular dependency: #{(path + [package.name]).join(' -> ')}" if path.include?(package.name)
|
|
33
|
-
|
|
34
|
-
package.dependencies.each do |dep_path|
|
|
35
|
-
full_path = File.join(@root_path, dep_path)
|
|
36
|
-
raise "Package not found: #{dep_path}" unless File.directory?(full_path)
|
|
37
|
-
|
|
38
|
-
resolve_dependencies(load_package(File.basename(dep_path), full_path), path + [package.name])
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def visit(package, visited, order, path)
|
|
43
|
-
return if visited[package.name]
|
|
44
|
-
|
|
45
|
-
visited[package.name] = true
|
|
46
|
-
package.dependencies.each do |dep_path|
|
|
47
|
-
dep_package = @packages[File.basename(dep_path)]
|
|
48
|
-
visit(dep_package, visited, order, path + [package.name]) if dep_package
|
|
49
|
-
end
|
|
50
|
-
order << package
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
data/lib/boxwerk/loader.rb
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Boxwerk
|
|
4
|
-
# Loader creates isolated Ruby::Box instances for packages and wires imports.
|
|
5
|
-
# Lazily loads exports using Zeitwerk naming conventions and injects constants.
|
|
6
|
-
class Loader
|
|
7
|
-
class << self
|
|
8
|
-
def boot_all(graph, registry)
|
|
9
|
-
order = graph.topological_order
|
|
10
|
-
|
|
11
|
-
order.each { |package| boot(package, graph, registry) }
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def boot(package, graph, registry)
|
|
15
|
-
return package if package.booted?
|
|
16
|
-
|
|
17
|
-
package.box = Ruby::Box.new
|
|
18
|
-
wire_imports(package.box, package, graph)
|
|
19
|
-
registry.register(package.name, package)
|
|
20
|
-
|
|
21
|
-
package
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def load_export(package, const_name)
|
|
27
|
-
# Skip if already loaded (cached)
|
|
28
|
-
return if package.loaded_exports.key?(const_name)
|
|
29
|
-
|
|
30
|
-
lib_path = File.join(package.path, 'lib')
|
|
31
|
-
return unless File.directory?(lib_path)
|
|
32
|
-
|
|
33
|
-
file_path = find_file_for_constant(lib_path, const_name)
|
|
34
|
-
unless file_path
|
|
35
|
-
raise "Cannot find file for exported constant '#{const_name}' in package '#{package.name}'"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
package.box.require(file_path)
|
|
39
|
-
package.loaded_exports[const_name] = file_path
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def find_file_for_constant(lib_path, const_name)
|
|
43
|
-
if const_name.include?('::')
|
|
44
|
-
nested_path = File.join(lib_path, "#{underscore(const_name)}.rb")
|
|
45
|
-
return nested_path if File.exist?(nested_path)
|
|
46
|
-
|
|
47
|
-
parts = const_name.split('::')
|
|
48
|
-
parent_name = parts[0..-2].join('::')
|
|
49
|
-
parent_file = File.join(lib_path, "#{underscore(parent_name)}.rb")
|
|
50
|
-
return parent_file if File.exist?(parent_file)
|
|
51
|
-
else
|
|
52
|
-
conventional_path =
|
|
53
|
-
File.join(lib_path, "#{underscore(const_name)}.rb")
|
|
54
|
-
return conventional_path if File.exist?(conventional_path)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
nil
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def underscore(string)
|
|
61
|
-
string
|
|
62
|
-
.gsub(/::/, '/')
|
|
63
|
-
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
64
|
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
65
|
-
.tr('-', '_')
|
|
66
|
-
.downcase
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def wire_imports(box, package, graph)
|
|
70
|
-
package.imports.each do |import_item|
|
|
71
|
-
path, config =
|
|
72
|
-
(
|
|
73
|
-
if import_item.is_a?(String)
|
|
74
|
-
[import_item, nil]
|
|
75
|
-
else
|
|
76
|
-
[import_item.keys.first, import_item.values.first]
|
|
77
|
-
end
|
|
78
|
-
)
|
|
79
|
-
dep_name = File.basename(path)
|
|
80
|
-
dependency = graph.packages[dep_name]
|
|
81
|
-
|
|
82
|
-
unless dependency
|
|
83
|
-
raise "Cannot resolve dependency '#{path}' for package '#{package.name}'"
|
|
84
|
-
end
|
|
85
|
-
unless dependency.booted?
|
|
86
|
-
raise "Dependency '#{dep_name}' not booted yet"
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
wire_import_strategy(box, package, path, config, dependency)
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def wire_import_strategy(box, package, path, config, dependency)
|
|
94
|
-
case config
|
|
95
|
-
when nil
|
|
96
|
-
create_namespace(package, camelize(File.basename(path)), dependency)
|
|
97
|
-
when String
|
|
98
|
-
create_namespace(package, config, dependency)
|
|
99
|
-
when Array
|
|
100
|
-
config.each do |name|
|
|
101
|
-
set_constant(package, name, get_constant(dependency, name))
|
|
102
|
-
end
|
|
103
|
-
when Hash
|
|
104
|
-
config.each do |remote, local|
|
|
105
|
-
set_constant(package, local, get_constant(dependency, remote))
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def create_namespace(package, namespace_name, dependency)
|
|
111
|
-
if dependency.exports.size == 1
|
|
112
|
-
set_constant(
|
|
113
|
-
package,
|
|
114
|
-
namespace_name,
|
|
115
|
-
get_constant(dependency, dependency.exports.first),
|
|
116
|
-
)
|
|
117
|
-
else
|
|
118
|
-
mod = create_module(package.box, namespace_name)
|
|
119
|
-
dependency.exports.each do |name|
|
|
120
|
-
mod.const_set(name.to_sym, get_constant(dependency, name))
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def get_constant(package, name)
|
|
126
|
-
load_export(package, name)
|
|
127
|
-
package.box.const_get(name.to_sym)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def set_constant(package, name, value)
|
|
131
|
-
package.box.const_set(name.to_sym, value)
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def create_module(box, name)
|
|
135
|
-
if box.const_defined?(name.to_sym, false)
|
|
136
|
-
return box.const_get(name.to_sym)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
mod = Module.new
|
|
140
|
-
box.const_set(name.to_sym, mod)
|
|
141
|
-
mod
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def camelize(string)
|
|
145
|
-
string.split('_').map(&:capitalize).join
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
end
|
data/lib/boxwerk/registry.rb
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Boxwerk
|
|
4
|
-
# Registry tracks booted package instances to ensure each package boots only once.
|
|
5
|
-
class Registry
|
|
6
|
-
def initialize
|
|
7
|
-
@registry = {}
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def register(name, instance)
|
|
11
|
-
@registry[name] = instance
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def get(name)
|
|
15
|
-
@registry[name]
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def registered?(name)
|
|
19
|
-
@registry.key?(name)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def clear!
|
|
23
|
-
@registry = {}
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|