ruby_workspace_manager 0.6.1 → 0.6.2
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/README.md +97 -22
- data/lib/ruby_workspace_manager.rb +3 -0
- data/lib/rwm/dependency_graph.rb +19 -2
- data/lib/rwm/gemfile.rb +16 -0
- data/lib/rwm/rails.rb +15 -4
- data/lib/rwm/task_cache.rb +8 -2
- data/lib/rwm/task_runner.rb +3 -0
- data/lib/rwm/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 659d59c0a587e4479114fd819337775db56f4c4ae634757d0c6d6206e46addb1
|
|
4
|
+
data.tar.gz: '07599b434c9f0f4d2aead2160cdf32029e24cf38701e1e66f1bacc9375c9fb2f'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8ee35a4451cabe1c66eaae86b1ceee7f64d9009a74e79bb744e27b21208fc7897b31f20df90e80a2546d760ae7e122a9f314455c3354d36fbd3cb426e508f62a
|
|
7
|
+
data.tar.gz: 1acd2eadb3a504f83cdae5990f02b69bb38304e8e2def62b494099131982e93e7569b7be41198065d1fd132cc012585ecdc033044bd1ffb38d94df4f9d29675b
|
data/README.md
CHANGED
|
@@ -610,60 +610,135 @@ Exits 0 on pass, 1 on violation. The pre-push hook runs this automatically.
|
|
|
610
610
|
|
|
611
611
|
## Rails and Zeitwerk
|
|
612
612
|
|
|
613
|
-
|
|
613
|
+
### How workspace libs work in Rails
|
|
614
614
|
|
|
615
|
-
|
|
615
|
+
Workspace libs declared via `rwm_lib` are path gems. The standard Rails boot sequence handles them automatically:
|
|
616
616
|
|
|
617
|
-
|
|
617
|
+
1. `config/boot.rb` calls `Bundler.setup` — adds all gem `lib/` directories to `$LOAD_PATH`
|
|
618
|
+
2. `config/application.rb` calls `Bundler.require(*Rails.groups)` — auto-requires every gem, including workspace libs and their transitive deps
|
|
619
|
+
3. `config/environment.rb` calls `Rails.application.initialize!` — Zeitwerk activates for the app's own code
|
|
620
|
+
|
|
621
|
+
By the time Zeitwerk starts in step 3, workspace libs are already loaded as plain Ruby modules. Zeitwerk never touches them — it only manages directories in `config.autoload_paths`.
|
|
622
|
+
|
|
623
|
+
**No special setup is needed in `application.rb`.** A standard Rails template works:
|
|
618
624
|
|
|
619
625
|
```ruby
|
|
620
626
|
# apps/web/Gemfile
|
|
627
|
+
require "rwm/gemfile"
|
|
628
|
+
|
|
621
629
|
source "https://rubygems.org"
|
|
622
630
|
gemspec
|
|
623
631
|
|
|
624
|
-
require "rwm/gemfile"
|
|
625
|
-
|
|
626
632
|
rwm_lib "auth" # transitive deps resolved automatically
|
|
627
633
|
```
|
|
628
634
|
|
|
629
|
-
|
|
635
|
+
```ruby
|
|
636
|
+
# apps/web/config/application.rb
|
|
637
|
+
require_relative "boot"
|
|
638
|
+
require "rails/all"
|
|
639
|
+
Bundler.require(*Rails.groups)
|
|
640
|
+
|
|
641
|
+
module Web
|
|
642
|
+
class Application < Rails::Application
|
|
643
|
+
config.load_defaults 8.0
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
That's it. `Bundler.require` loads `auth` and all of its transitive workspace dependencies. No manual `Rwm.require_libs`, no ordering tricks.
|
|
649
|
+
|
|
650
|
+
### A note on Zeitwerk
|
|
651
|
+
|
|
652
|
+
> [!IMPORTANT]
|
|
653
|
+
> **Correction (v0.6.2):** Documentation in v0.6.1 and earlier incorrectly stated that Zeitwerk overrides `Kernel#require`. This was wrong. Zeitwerk uses `Module#autoload` and `const_missing` to lazily load files from `config.autoload_paths`. A plain `require "auth"` (from `Bundler.require` or anywhere else) works normally at any point during the boot sequence — Zeitwerk does not intercept it.
|
|
654
|
+
|
|
655
|
+
### The practical lib workflow
|
|
656
|
+
|
|
657
|
+
**Develop inside your Rails app first.** While a feature is in active development, keep the code in your Rails app's `app/` directory where Zeitwerk gives you hot reloading for free. Change a file, refresh the page, see the result.
|
|
658
|
+
|
|
659
|
+
**Extract when stable.** When the code has solidified — the interface is settled, multiple apps could use it, you're not changing it every day — extract it into a workspace lib. This is the natural monorepo rhythm: apps are where you experiment, libs are where you consolidate.
|
|
660
|
+
|
|
661
|
+
At extraction time, choose how the lib is structured.
|
|
662
|
+
|
|
663
|
+
### Traditional structure (the default)
|
|
664
|
+
|
|
665
|
+
This is what `rwm new lib` scaffolds. The lib's entry point loads all sub-files eagerly with `require_relative`:
|
|
630
666
|
|
|
631
667
|
```ruby
|
|
632
|
-
#
|
|
633
|
-
|
|
668
|
+
# libs/auth/lib/auth.rb
|
|
669
|
+
require_relative "auth/token"
|
|
670
|
+
require_relative "auth/user"
|
|
671
|
+
|
|
672
|
+
module Auth
|
|
673
|
+
VERSION = "0.1.0"
|
|
674
|
+
end
|
|
634
675
|
```
|
|
635
676
|
|
|
636
|
-
**
|
|
677
|
+
**Pros:** Works everywhere — Rails, non-Rails, any Ruby app. Simple. Standard gem structure.
|
|
678
|
+
|
|
679
|
+
**Cons:** No hot reloading in Rails development. After changing a lib file, you restart the server. This is fine for stable extracted code — you're not changing it often.
|
|
680
|
+
|
|
681
|
+
This is the right choice for most workspace libs.
|
|
682
|
+
|
|
683
|
+
### Zeitwerk-compatible structure (opt-in)
|
|
684
|
+
|
|
685
|
+
Choose this when you're still actively iterating on a lib **and** multiple Rails apps consume it. The lib follows Zeitwerk naming conventions — one constant per file, no `require_relative`:
|
|
637
686
|
|
|
638
687
|
```ruby
|
|
639
|
-
#
|
|
640
|
-
|
|
688
|
+
# libs/auth/lib/auth.rb
|
|
689
|
+
module Auth
|
|
690
|
+
end
|
|
641
691
|
|
|
642
|
-
|
|
643
|
-
|
|
692
|
+
# libs/auth/lib/auth/token.rb — defines Auth::Token
|
|
693
|
+
# libs/auth/lib/auth/user.rb — defines Auth::User
|
|
694
|
+
# Zeitwerk auto-discovers these. No require lines needed.
|
|
695
|
+
```
|
|
644
696
|
|
|
645
|
-
require
|
|
646
|
-
require "action_controller/railtie"
|
|
697
|
+
Each consuming Rails app opts in by adding the lib to its autoload paths and telling Bundler not to auto-require it:
|
|
647
698
|
|
|
699
|
+
```ruby
|
|
700
|
+
# apps/web/Gemfile
|
|
701
|
+
rwm_lib "auth", require: false # Bundler won't auto-require
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
```ruby
|
|
705
|
+
# apps/web/config/application.rb
|
|
648
706
|
module Web
|
|
649
707
|
class Application < Rails::Application
|
|
650
|
-
config.
|
|
708
|
+
config.autoload_paths << Rwm.lib_path("auth")
|
|
709
|
+
config.eager_load_paths << Rwm.lib_path("auth")
|
|
651
710
|
end
|
|
652
711
|
end
|
|
653
712
|
```
|
|
654
713
|
|
|
655
|
-
|
|
714
|
+
Now Zeitwerk manages `auth` — lazy loading in development (with hot reloading), eager loading in production. Changes to lib files are picked up on the next request without restarting the server.
|
|
656
715
|
|
|
657
|
-
|
|
716
|
+
**Trade-offs:**
|
|
658
717
|
|
|
659
|
-
|
|
718
|
+
- All consumer apps must add the lib to their autoload paths — this is a per-app decision
|
|
719
|
+
- The lib cannot use `require_relative` for its own files (Zeitwerk must control loading)
|
|
720
|
+
- Non-Rails consumers need a different loading strategy (e.g., `Zeitwerk::Loader.for_gem` or a `Dir.glob` require)
|
|
660
721
|
|
|
661
722
|
### What doesn't work
|
|
662
723
|
|
|
663
|
-
|
|
664
|
-
|
|
724
|
+
**Mixing `Bundler.require` and `autoload_paths` for the same lib.** If `Bundler.require` loads a lib (the default) and you also add it to `config.autoload_paths`, the lib's constants are loaded twice — once eagerly by Bundler, once lazily by Zeitwerk. Reloading breaks because Zeitwerk didn't control the initial load. Pick one or the other per lib.
|
|
725
|
+
|
|
726
|
+
**Using `require_relative` inside a Zeitwerk-managed lib.** Initial loading works fine — Zeitwerk tolerates other loading mechanisms. But after a Zeitwerk reload cycle (in development), files loaded by `require_relative` are still in `$LOADED_FEATURES`. Ruby's `require_relative` sees them as already loaded and skips them. The constants were removed by Zeitwerk's reload but never re-defined. Result: `NameError`.
|
|
727
|
+
|
|
728
|
+
### `Rwm.require_libs` — when you need it
|
|
729
|
+
|
|
730
|
+
For standard Rails apps, `Bundler.require` handles everything. `Rwm.require_libs` exists for edge cases:
|
|
731
|
+
|
|
732
|
+
- Non-standard Rails setups that don't call `Bundler.require`
|
|
733
|
+
- Non-Rails apps that want to load all workspace libs in one call
|
|
734
|
+
- Explicit control over when workspace libs are loaded
|
|
735
|
+
|
|
736
|
+
```ruby
|
|
737
|
+
require "rwm/rails"
|
|
738
|
+
Rwm.require_libs # requires all libs resolved by rwm_lib, idempotent
|
|
739
|
+
```
|
|
665
740
|
|
|
666
|
-
Non-Rails apps don't
|
|
741
|
+
Non-Rails apps don't need any of this — `require` workspace libs from your Gemfile anywhere in your code, as with any gem.
|
|
667
742
|
|
|
668
743
|
## VSCode integration
|
|
669
744
|
|
data/lib/rwm/dependency_graph.rb
CHANGED
|
@@ -103,13 +103,30 @@ module Rwm
|
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
Rwm.debug("graph: loading from cache at #{path}")
|
|
106
|
-
|
|
106
|
+
begin
|
|
107
|
+
data = JSON.parse(read_locked(path))
|
|
108
|
+
rescue Errno::ENOENT
|
|
109
|
+
Rwm.debug("graph: cache file disappeared, rebuilding")
|
|
110
|
+
return build_and_save(workspace)
|
|
111
|
+
rescue JSON::ParserError
|
|
112
|
+
Rwm.debug("graph: cache file contains invalid JSON, rebuilding")
|
|
113
|
+
return build_and_save(workspace)
|
|
114
|
+
end
|
|
115
|
+
|
|
107
116
|
graph = new
|
|
108
117
|
|
|
109
118
|
workspace.packages.each { |pkg| graph.add_package(pkg) }
|
|
110
119
|
|
|
111
120
|
data["edges"]&.each do |name, deps|
|
|
112
|
-
|
|
121
|
+
next unless graph.packages.key?(name)
|
|
122
|
+
|
|
123
|
+
deps.each do |dep|
|
|
124
|
+
if graph.packages.key?(dep)
|
|
125
|
+
graph.add_edge(name, dep)
|
|
126
|
+
else
|
|
127
|
+
Rwm.debug("graph: skipping stale edge #{name} -> #{dep} (package removed)")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
113
130
|
end
|
|
114
131
|
|
|
115
132
|
graph
|
data/lib/rwm/gemfile.rb
CHANGED
|
@@ -17,17 +17,33 @@ require "set"
|
|
|
17
17
|
|
|
18
18
|
module Rwm
|
|
19
19
|
@resolved_libs = Set.new
|
|
20
|
+
@workspace_root = nil
|
|
20
21
|
|
|
21
22
|
def self.resolved_libs
|
|
22
23
|
@resolved_libs
|
|
23
24
|
end
|
|
24
25
|
|
|
26
|
+
def self.workspace_root
|
|
27
|
+
@workspace_root
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.workspace_root=(path)
|
|
31
|
+
@workspace_root = path
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.lib_path(name)
|
|
35
|
+
raise "rwm: workspace root not set (was rwm_lib used in the Gemfile?)" unless @workspace_root
|
|
36
|
+
|
|
37
|
+
File.join(@workspace_root, "libs", name.to_s, "lib")
|
|
38
|
+
end
|
|
39
|
+
|
|
25
40
|
module GemfileDsl
|
|
26
41
|
def rwm_workspace_root
|
|
27
42
|
@rwm_workspace_root ||= begin
|
|
28
43
|
out, _, status = Open3.capture3("git", "rev-parse", "--show-toplevel")
|
|
29
44
|
root = status.success? ? out.strip : ""
|
|
30
45
|
raise "rwm: not inside a git repository" if root.empty?
|
|
46
|
+
Rwm.workspace_root ||= root
|
|
31
47
|
root
|
|
32
48
|
end
|
|
33
49
|
end
|
data/lib/rwm/rails.rb
CHANGED
|
@@ -2,17 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
# Rails integration for RWM workspaces.
|
|
4
4
|
#
|
|
5
|
-
#
|
|
5
|
+
# For standard Rails apps, Bundler.require handles workspace libs
|
|
6
|
+
# automatically — no manual require_libs call is needed.
|
|
7
|
+
#
|
|
8
|
+
# This file is for non-standard setups or explicit control:
|
|
6
9
|
#
|
|
7
|
-
# require_relative "boot"
|
|
8
10
|
# require "rwm/rails"
|
|
9
11
|
# Rwm.require_libs
|
|
10
|
-
# require "rails"
|
|
11
12
|
|
|
12
13
|
require "rwm/gemfile"
|
|
13
14
|
|
|
14
15
|
module Rwm
|
|
16
|
+
@libs_required = false
|
|
17
|
+
|
|
18
|
+
def self.libs_required?
|
|
19
|
+
@libs_required
|
|
20
|
+
end
|
|
21
|
+
|
|
15
22
|
def self.require_libs
|
|
16
|
-
|
|
23
|
+
return if @libs_required
|
|
24
|
+
|
|
25
|
+
resolved_libs.each { |name| require name }
|
|
26
|
+
@libs_required = true
|
|
27
|
+
debug("required #{resolved_libs.size} workspace lib(s): #{resolved_libs.to_a.sort.join(', ')}")
|
|
17
28
|
end
|
|
18
29
|
end
|
data/lib/rwm/task_cache.rb
CHANGED
|
@@ -24,6 +24,7 @@ module Rwm
|
|
|
24
24
|
@graph = graph
|
|
25
25
|
@cache_dir = File.join(workspace.root, ".rwm", "cache")
|
|
26
26
|
@content_hashes = {}
|
|
27
|
+
@content_hash_mutex = Mutex.new
|
|
27
28
|
@cache_declarations = {}
|
|
28
29
|
@declarations_mutex = Mutex.new
|
|
29
30
|
end
|
|
@@ -78,7 +79,9 @@ module Rwm
|
|
|
78
79
|
|
|
79
80
|
# Compute a content hash for a package: SHA256 of all source files + dependency hashes
|
|
80
81
|
def content_hash(package)
|
|
81
|
-
|
|
82
|
+
@content_hash_mutex.synchronize do
|
|
83
|
+
return @content_hashes[package.name] if @content_hashes.key?(package.name)
|
|
84
|
+
end
|
|
82
85
|
|
|
83
86
|
digest = Digest::SHA256.new
|
|
84
87
|
|
|
@@ -97,7 +100,10 @@ module Rwm
|
|
|
97
100
|
digest.update(content_hash(dep_pkg))
|
|
98
101
|
end
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
computed = digest.hexdigest
|
|
104
|
+
@content_hash_mutex.synchronize do
|
|
105
|
+
@content_hashes[package.name] = computed
|
|
106
|
+
end
|
|
101
107
|
end
|
|
102
108
|
|
|
103
109
|
# Preload cache declarations for multiple packages in parallel.
|
data/lib/rwm/task_runner.rb
CHANGED
|
@@ -73,6 +73,9 @@ module Rwm
|
|
|
73
73
|
running[pkg.name] = Thread.new do
|
|
74
74
|
begin
|
|
75
75
|
result = run_single(pkg, &command_proc)
|
|
76
|
+
rescue IOError
|
|
77
|
+
# Thread killed during I/O (Ctrl+C) — suppress noise
|
|
78
|
+
next
|
|
76
79
|
rescue => e
|
|
77
80
|
result = Result.new(
|
|
78
81
|
package_name: pkg.name, task: "error",
|
data/lib/rwm/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_workspace_manager
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Siddharth Bhatt
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: tsort
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
12
26
|
description: Convention-over-configuration monorepo tool for Ruby. Manages dependency
|
|
13
27
|
graphs, runs tasks in parallel, detects affected packages, and enforces structural
|
|
14
28
|
conventions.
|
|
@@ -22,6 +36,7 @@ files:
|
|
|
22
36
|
- bin/rwm
|
|
23
37
|
- completions/rwm.bash
|
|
24
38
|
- completions/rwm.zsh
|
|
39
|
+
- lib/ruby_workspace_manager.rb
|
|
25
40
|
- lib/rwm.rb
|
|
26
41
|
- lib/rwm/affected_detector.rb
|
|
27
42
|
- lib/rwm/cli.rb
|