Class: Shadwire::Dependencies
- Inherits:
-
Object
- Object
- Shadwire::Dependencies
- Defined in:
- lib/shadwire/dependencies.rb
Overview
Applies a component's non-file dependencies to a consuming Rails app: gems
(via bundle add), importmap pins, and the Tailwind @import. Each method
detects what is missing, then applies only when told to (yes: or a truthy
confirm: callback) — the command owns the prompting. On an unsupported
stack (no importmap / no tailwindcss-rails) it returns manual instructions
and mutates nothing.
Every method returns a normalized result hash:
{ applied: [...], skipped: [...], pending: [...], manual: [...], failed: [...] }
failed carries the things we tried to apply and could not — a bundle add
that exited non-zero, above all. Callers must surface it and exit non-zero:
reporting a gem as installed when it is not leaves the app raising
uninitialized constant ViewComponent at request time, which looks unrelated
to the install that caused it.
Constant Summary collapse
- DEFAULT_RUNNER =
Runs a shell command in the app root, isolated from the CLI's own bundle.
lambda do |cmd, chdir:| require "bundler" Bundler.with_unbundled_env { system(*cmd, chdir: chdir) } end
Class Method Summary collapse
-
.raise_on_failure!(*results) ⇒ Object
Raises when any of the given result hashes reports something it failed to apply, so a command that could not install a dependency exits non-zero instead of claiming success.
Instance Method Summary collapse
-
#ensure_gems(names, yes:, confirm: nil) ⇒ Object
Ensures each gem name is present, running
bundle add <missing...>. -
#ensure_importmap_pins(pins, yes:, confirm: nil) ⇒ Object
Ensures each pin ({ "name" =>, "to" => }) exists in config/importmap.rb.
-
#ensure_tailwind_import(vendor_css_target, css_entry: "app/assets/tailwind/application.css", yes:, confirm: nil) ⇒ Object
Ensures
@import "<relative>";(pointing at the vendored shadwire.css) appears after@import "tailwindcss";in the Tailwind css entry. -
#initialize(project, runner: DEFAULT_RUNNER) ⇒ Dependencies
constructor
A new instance of Dependencies.
Constructor Details
#initialize(project, runner: DEFAULT_RUNNER) ⇒ Dependencies
Returns a new instance of Dependencies.
39 40 41 42 |
# File 'lib/shadwire/dependencies.rb', line 39 def initialize(project, runner: DEFAULT_RUNNER) @project = project @runner = runner end |
Class Method Details
.raise_on_failure!(*results) ⇒ Object
Raises when any of the given result hashes reports something it failed to apply, so a command that could not install a dependency exits non-zero instead of claiming success.
31 32 33 34 35 36 37 |
# File 'lib/shadwire/dependencies.rb', line 31 def self.raise_on_failure!(*results) failed = results.flat_map { |result| Array(result[:failed]) }.uniq return if failed.empty? raise Error, "Failed to install: #{failed.join(", ")}. " \ "Run `bundle add #{failed.join(" ")}` in the app and re-run this command." end |
Instance Method Details
#ensure_gems(names, yes:, confirm: nil) ⇒ Object
Ensures each gem name is present, running bundle add <missing...>.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/shadwire/dependencies.rb', line 45 def ensure_gems(names, yes:, confirm: nil) present = names.select { |name| @project.gem?(name) } missing = names - present return result(skipped: present) if missing.empty? unless apply?(yes, confirm, "Add gems: #{missing.join(", ")} (bundle add)") return result(skipped: present, pending: missing) end # `system` returns false on a non-zero exit and nil when the command could # not be run at all; neither means the gems landed. return result(skipped: present, failed: missing) unless @runner.call(["bundle", "add", *missing], chdir: @project.root) result(applied: missing, skipped: present) end |
#ensure_importmap_pins(pins, yes:, confirm: nil) ⇒ Object
Ensures each pin ({ "name" =>, "to" => }) exists in config/importmap.rb.
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/shadwire/dependencies.rb', line 62 def ensure_importmap_pins(pins, yes:, confirm: nil) # importmap? is true when the gem is in the Gemfile, but config/importmap.rb # only exists once `importmap:install` has run — guard the read so an # unconfigured app falls back to a manual instruction instead of crashing. path = @project.importmap_path unless @project.importmap? && File.exist?(path) return result(manual: pins.map { |p| manual_pin_instruction(p) }) end content = File.read(path) present, missing = pins.partition { |pin| pin_present?(content, pin) } return result(skipped: present.map { |p| p["name"] }) if missing.empty? names = missing.map { |p| p["name"] } unless apply?(yes, confirm, "Add importmap pins: #{names.join(", ")}") return result(skipped: present.map { |p| p["name"] }, pending: names) end appended = content appended += "\n" unless appended.end_with?("\n") missing.each { |pin| appended += %(pin "#{pin["name"]}", to: "#{pin["to"]}"\n) } File.write(path, appended) result(applied: names, skipped: present.map { |p| p["name"] }) end |
#ensure_tailwind_import(vendor_css_target, css_entry: "app/assets/tailwind/application.css", yes:, confirm: nil) ⇒ Object
Ensures @import "<relative>"; (pointing at the vendored shadwire.css)
appears after @import "tailwindcss"; in the Tailwind css entry.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/shadwire/dependencies.rb', line 89 def ensure_tailwind_import(vendor_css_target, css_entry: "app/assets/tailwind/application.css", yes:, confirm: nil) css_path = @project.tailwind_css_path(css_entry) unless @project.tailwindcss_rails? && File.exist?(css_path) return result(manual: [manual_tailwind_instruction(vendor_css_target, css_entry)]) end relative = relative_import(vendor_css_target, css_entry) content = File.read(css_path) return result(skipped: [relative]) if content.include?(%(@import "#{relative}")) unless apply?(yes, confirm, "Add Tailwind import: #{relative}") return result(pending: [relative]) end File.write(css_path, insert_import(content, relative)) result(applied: [relative]) end |