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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7292181b24a08aeaed78ff41ee3fe3d3284185af153d5c151cd64f6f2bda6948
4
- data.tar.gz: 41c43c206dbfbbd9ac68f1d9378bab8820d17386dfa8f025be66bc4eeb628a35
3
+ metadata.gz: 659d59c0a587e4479114fd819337775db56f4c4ae634757d0c6d6206e46addb1
4
+ data.tar.gz: '07599b434c9f0f4d2aead2160cdf32029e24cf38701e1e66f1bacc9375c9fb2f'
5
5
  SHA512:
6
- metadata.gz: 6c72d4000b74f07310757f950e9a081eac2697574c9600bdcfd6d0d1aa1fe0b86b4a2a40b11ef9e9ce5df7762e72fe505e4c88b653bfde16e4d2df8fd343ce07
7
- data.tar.gz: 3c40d200f9bf6de9da5f8f45d0147b57b395d95507d3d61e449abcf0317d27cd629b544a4289b711948ad19453994d6cf9fe790f24434d6a650510027b1ed5a6
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
- Rails uses [Zeitwerk](https://github.com/fxn/zeitwerk) for autoloading. Zeitwerk overrides `Kernel#require`, so `require "auth"` in a controller will fail with a `LoadError` even though the gem is installed. The fix: require workspace libs before Zeitwerk starts.
613
+ ### How workspace libs work in Rails
614
614
 
615
- ### Setup
615
+ Workspace libs declared via `rwm_lib` are path gems. The standard Rails boot sequence handles them automatically:
616
616
 
617
- **1. Gemfile**declare workspace deps with `rwm_lib`:
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
- `ruby_workspace_manager` must be a runtime dependency (not in `:development` group) for Rails apps:
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
- # apps/web/web.gemspec
633
- spec.add_dependency "ruby_workspace_manager"
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
- **2. application.rb**require workspace libs before Rails loads:
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
- # apps/web/config/application.rb
640
- require_relative "boot"
688
+ # libs/auth/lib/auth.rb
689
+ module Auth
690
+ end
641
691
 
642
- require "rwm/rails"
643
- Rwm.require_libs
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 "rails"
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.load_defaults 8.0
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
- `Rwm.require_libs` requires exactly the libs that `rwm_lib` resolved in the Gemfile direct and transitive. After this line, workspace libraries are loaded as plain Ruby modules. Zeitwerk takes over for the app's own code and never touches them.
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
- ### Why this ordering matters
716
+ **Trade-offs:**
658
717
 
659
- The Rails boot sequence: `config/boot.rb` runs `Bundler.setup` (adds gems to load path) `config/application.rb` (your code, then Rails) → `config/environment.rb` (Zeitwerk activates). `Rwm.require_libs` must run after Bundler.setup and before `require "rails"`.
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
- - `require "auth"` inside a controller or model — Zeitwerk is already active.
664
- - Adding workspace libs to `config.autoload_paths` — they have their own structure.
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 have this problem and can `require` workspace libs defined in their Gemfile anywhere in their code.
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
 
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rwm"
@@ -103,13 +103,30 @@ module Rwm
103
103
  end
104
104
 
105
105
  Rwm.debug("graph: loading from cache at #{path}")
106
- data = JSON.parse(read_locked(path))
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
- deps.each { |dep| graph.add_edge(name, dep) }
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
- # Usage in config/application.rb:
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
- self.resolved_libs.each { |name| require name }
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
@@ -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
- return @content_hashes[package.name] if @content_hashes.key?(package.name)
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
- @content_hashes[package.name] = digest.hexdigest
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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rwm
4
- VERSION = "0.6.1"
4
+ VERSION = "0.6.2"
5
5
  end
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.1
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