Class: Browsable::Doctor

Inherits:
Object
  • Object
show all
Defined in:
lib/browsable/doctor.rb

Overview

Verifies that the external tools browsable shells out to are installed, and guides the user through installing whatever is missing.

Herb is a gem dependency and runs in-process, so it is never checked here —only node, npm, stylelint, eslint and eslint-plugin-compat.

Defined Under Namespace

Classes: Status, Tool

Constant Summary collapse

TOOLS =
[
  Tool.new(key: :node, label: "node", binary: "node", npm_package: nil,
           purpose: "JavaScript runtime that stylelint and eslint run on",
           enables: %i[css js], required: true),
  Tool.new(key: :npm, label: "npm", binary: "npm", npm_package: nil,
           purpose: "installs the CSS/JS tooling (used by `doctor --fix`)",
           enables: [], required: false),
  Tool.new(key: :stylelint, label: "stylelint", binary: "stylelint",
           npm_package: "stylelint stylelint-no-unsupported-browser-features",
           purpose: "audits CSS for unsupported browser features",
           enables: %i[css], required: true),
  Tool.new(key: :eslint, label: "eslint", binary: "eslint",
           npm_package: "eslint eslint-plugin-compat",
           purpose: "audits JavaScript for unsupported browser features",
           enables: %i[js], required: true),
  Tool.new(key: :eslint_plugin_compat, label: "eslint-plugin-compat", binary: nil,
           npm_package: "eslint-plugin-compat",
           purpose: "the eslint plugin that performs the JS compat checks",
           enables: %i[js], required: true)
].freeze
ALWAYS_AVAILABLE =

Analyzer kinds that need no external tooling at all.

%i[erb html].freeze

Instance Method Summary collapse

Instance Method Details

#available_kindsObject

Which analyzer kinds can actually run on this machine right now.



68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/browsable/doctor.rb', line 68

def available_kinds
  # In dry-run mode the external tools are never invoked, so treat them all
  # as available — this keeps specs and `BROWSABLE_DRY_RUN` audits working.
  return %i[css erb html js] if ENV.key?("BROWSABLE_DRY_RUN")

  kinds = ALWAYS_AVAILABLE.dup
  %i[css js].each do |kind|
    needed = TOOLS.select { |tool| tool.enables.include?(kind) }
    kinds << kind if needed.all? { |tool| installed?(tool) }
  end
  kinds
end

#fix!(io: $stdout, input: $stdin, assume_yes: false) ⇒ Object

Attempt to install everything that is missing. Runnable commands only (npm/brew); anything that needs a manual download is reported, not run.



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/browsable/doctor.rb', line 106

def fix!(io: $stdout, input: $stdin, assume_yes: false)
  return true if ok?

  runnable, manual = install_commands.partition { |cmd| cmd.start_with?("npm ", "brew ") }

  manual.each { |cmd| io.puts "Manual step required: #{cmd}" }
  return ok? if runnable.empty?

  io.puts "browsable will run:"
  runnable.each { |cmd| io.puts "  #{cmd}" }
  unless assume_yes
    io.print "Proceed? [y/N] "
    answer = input.gets&.strip&.downcase
    return false unless %w[y yes].include?(answer)
  end

  runnable.each do |cmd|
    io.puts "+ #{cmd}"
    system(cmd)
  end
  @statuses = nil
  @installed_cache = nil
  ok?
end

#missingObject



63
64
65
# File 'lib/browsable/doctor.rb', line 63

def missing
  statuses.reject(&:installed?).map(&:tool)
end

#ok?Boolean

True when every required tool is present.

Returns:

  • (Boolean)


59
60
61
# File 'lib/browsable/doctor.rb', line 59

def ok?
  statuses.select { |s| s.tool.required }.all?(&:installed?)
end

#render(color: $stdout.tty?) ⇒ Object

A formatted, colourised dependency report.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/browsable/doctor.rb', line 82

def render(color: $stdout.tty?)
  pastel = Pastel.new(enabled: color)
  lines = [pastel.bold("browsable doctor — system dependencies"), ""]

  statuses.each do |status|
    mark = status.installed? ? pastel.green("") : pastel.red("")
    lines << "  #{mark} #{pastel.bold(status.tool.label)}#{status.tool.purpose}"
    lines << pastel.dim("      #{status.detail}") if status.detail
  end

  lines << ""
  if ok?
    lines << pastel.green.bold("All required tools are installed. You're ready to audit.")
  else
    lines << pastel.red.bold("Missing required tools — install them with:")
    install_commands.each { |cmd| lines << "  #{pastel.cyan(cmd)}" }
    lines << ""
    lines << pastel.dim("Or run `browsable doctor --fix` to install them automatically.")
  end
  lines.join("\n")
end

#statusesObject



51
52
53
54
55
56
# File 'lib/browsable/doctor.rb', line 51

def statuses
  @statuses ||= TOOLS.map do |tool|
    present = installed?(tool)
    Status.new(tool: tool, installed: present, detail: detail_for(tool, present))
  end
end