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
-
.bundle_add_command(result) ⇒ Object
bundle add <names...>for a result, with its group when it has one. -
.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, group: 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.
52 53 54 55 |
# File 'lib/shadwire/dependencies.rb', line 52 def initialize(project, runner: DEFAULT_RUNNER) @project = project @runner = runner end |
Class Method Details
.bundle_add_command(result) ⇒ Object
bundle add <names...> for a result, with its group when it has one.
47 48 49 50 |
# File 'lib/shadwire/dependencies.rb', line 47 def self.bundle_add_command(result) suffix = result[:group] ? " --group #{result[:group]}" : "" "bundle add #{Array(result[:failed]).join(" ")}#{suffix}" end |
.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.
Each result carries its own group:, so the recovery command it suggests
matches the one that failed. Collapsing them would tell the user to
bundle add shadwire, landing the development-only CLI in the app's
runtime dependencies.
35 36 37 38 39 40 41 42 43 44 |
# File 'lib/shadwire/dependencies.rb', line 35 def self.raise_on_failure!(*results) failing = results.reject { |result| Array(result[:failed]).empty? } return if failing.empty? names = failing.flat_map { |result| Array(result[:failed]) }.uniq commands = failing.map { |result| bundle_add_command(result) } raise Error, "Failed to install: #{names.join(", ")}. " \ "Run `#{commands.join("` and `")}` in the app and re-run this command." end |
Instance Method Details
#ensure_gems(names, yes:, confirm: nil, group: nil) ⇒ Object
Ensures each gem name is present, running bundle add <missing...>.
group: adds them to a bundler group — used for the shadwire CLI itself,
which belongs in development, not in the app's runtime dependencies.
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/shadwire/dependencies.rb', line 60 def ensure_gems(names, yes:, confirm: nil, group: nil) present = names.select { |name| @project.gem?(name) } missing = names - present return result(skipped: present) if missing.empty? suffix = group ? " --group #{group}" : "" unless apply?(yes, confirm, "Add gems: #{missing.join(", ")} (bundle add#{suffix})") return result(skipped: present, pending: missing, group:) end command = ["bundle", "add", *missing] command.push("--group", group.to_s) if group # `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, group:) unless @runner.call(command, chdir: @project.root) result(applied: missing, skipped: present, group:) end |
#ensure_importmap_pins(pins, yes:, confirm: nil) ⇒ Object
Ensures each pin ({ "name" =>, "to" => }) exists in config/importmap.rb.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/shadwire/dependencies.rb', line 81 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.
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/shadwire/dependencies.rb', line 108 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 |