browsable 0.2.0 → 0.2.1
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/CHANGELOG.md +20 -0
- data/README.md +33 -0
- data/lib/browsable/analyzers/css.rb +14 -1
- data/lib/browsable/asset_pipeline.rb +77 -0
- data/lib/browsable/cli.rb +25 -7
- data/lib/browsable/config.rb +7 -2
- data/lib/browsable/doctor.rb +79 -5
- data/lib/browsable/formatters/human.rb +1 -0
- data/lib/browsable/replay.rb +1 -0
- data/lib/browsable/report.rb +6 -2
- data/lib/browsable/test_report.rb +2 -1
- data/lib/browsable/version.rb +1 -1
- data/lib/generators/browsable/install/templates/browsable.yml.tt +4 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e0e36477f3fd4bb8e494212d0bc76eb3b9bd7ec96fcb51af0e3e4e8df04e9428
|
|
4
|
+
data.tar.gz: 5f25664cd5d1e76644d77952844559de5ed95407d2d73e5a2ce2e9a8e18b9e22
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d1cb051b2e1c9e16f4dc56af0d76bf51d883860676f0877057a746a3029ff4ba8173f19b3f9d597171aa3541294ac05a96bd0807dc65eeedcd72def9e29c42b
|
|
7
|
+
data.tar.gz: dc5143e15d4dc5c17e2b1ecaa715b6413272df5bbb3e792ffafd90f6ff69696c599992f3cdbb62515afc6adc5567cd9d9b067112bd7292ddd15a7a9dcadb4ec6
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to the `browsable` gem are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.1] - 2026-05-25
|
|
9
|
+
|
|
10
|
+
### Added — Sprockets asset-pipeline support
|
|
11
|
+
|
|
12
|
+
- **Pipeline detection (`Browsable::AssetPipeline`).** Identifies whether the
|
|
13
|
+
project uses Propshaft, Sprockets, both, or neither. Surfaces the name in the
|
|
14
|
+
audit header (`pipeline: sprockets`) and as a top-level field in `--json`
|
|
15
|
+
output (`"pipeline": "sprockets"`). Live `defined?` checks win when available;
|
|
16
|
+
otherwise the `Gemfile.lock` is the fallback.
|
|
17
|
+
- **Sprockets-layout JS discovery.** Default JS globs now include
|
|
18
|
+
`app/assets/javascripts/**/*.{js,mjs}` alongside the Propshaft/importmap
|
|
19
|
+
`app/javascript/**`. Sprockets directives (`//= require`, `*= require_tree`)
|
|
20
|
+
live inside comments and are passed through untouched — no preprocessing.
|
|
21
|
+
- **SCSS routing.** `.scss` and `.sass` are discovered and routed to stylelint
|
|
22
|
+
with `--customSyntax postcss-scss` when any are present.
|
|
23
|
+
- **`postcss-scss` in `doctor`.** Listed as an optional dependency, only flagged
|
|
24
|
+
as missing when the project actually has SCSS files on disk.
|
|
25
|
+
- **`examples/sprockets_app`.** New fixture mirroring `examples/rails_app` with
|
|
26
|
+
the Sprockets layout.
|
|
27
|
+
|
|
8
28
|
## [0.2.0] - 2026-05-25
|
|
9
29
|
|
|
10
30
|
### Added — Runtime response auditing
|
data/README.md
CHANGED
|
@@ -31,6 +31,7 @@ The name is a play on Rails 8's `allow_browser` controller API. Instead of *decl
|
|
|
31
31
|
- [CLI reference](#cli-reference)
|
|
32
32
|
- [Configuration](#configuration)
|
|
33
33
|
- [How it works](#how-it-works)
|
|
34
|
+
- [Asset pipelines](#asset-pipelines)
|
|
34
35
|
- [Runtime auditing (test-suite mode)](#runtime-auditing-test-suite-mode)
|
|
35
36
|
- [Per-controller policies](#per-controller-and-per-action-policies)
|
|
36
37
|
- [Suggested policy fixes](#suggested-allow_browser-fix)
|
|
@@ -115,6 +116,7 @@ Browsable shells out to a few external tools that live globally on your machine:
|
|
|
115
116
|
| `node` | JavaScript runtime for `stylelint` & `eslint` | Yes |
|
|
116
117
|
| `stylelint` | CSS compatibility analysis | Yes (CSS audits) |
|
|
117
118
|
| `eslint` + `eslint-plugin-compat` | JavaScript compatibility analysis | Yes (JS audits) |
|
|
119
|
+
| `postcss-scss` | Lets `stylelint` parse SCSS sources | Optional (SCSS audits only) |
|
|
118
120
|
| `browserslist` | Live resolution of `defaults` queries | Optional |
|
|
119
121
|
| `herb` | ERB parsing | Bundled (gem dep) |
|
|
120
122
|
|
|
@@ -256,6 +258,37 @@ It's a suggestion, not an instruction. Tightening the policy is one fix; changin
|
|
|
256
258
|
|
|
257
259
|
The suggestion is derived from HTML/ERB findings, which carry exact version data. It also appears in `--json` output as `suggested_policy` and as a GitHub Actions notice.
|
|
258
260
|
|
|
261
|
+
## Asset pipelines
|
|
262
|
+
|
|
263
|
+
Browsable's audit pipeline (sources → analyzers → report) is **pipeline-agnostic**: the analyzers don't care how Rails assembles your assets. Only the static-mode source-discovery layer needs to know where to look.
|
|
264
|
+
|
|
265
|
+
| Pipeline | Static-mode support | What gets discovered |
|
|
266
|
+
| --- | --- | --- |
|
|
267
|
+
| **Propshaft** *(primary target)* | Full | `app/javascript/**`, `app/assets/stylesheets/**`, `app/assets/builds/**`, importmap pins |
|
|
268
|
+
| **Sprockets** | Full | `app/assets/javascripts/**`, `app/assets/stylesheets/**` (incl. `.scss`) |
|
|
269
|
+
| **Both (migration)** | Full — Sprockets discovery wins | Superset of both layouts |
|
|
270
|
+
| **Neither** | Best-effort | Whatever the default globs find on disk |
|
|
271
|
+
|
|
272
|
+
The detected pipeline appears in the audit header (e.g. `pipeline: sprockets`) and as a top-level field in `--json` output (`"pipeline": "sprockets"`). Detection prefers a live `defined?(Sprockets)` / `defined?(Propshaft)` check (set by the railtie inside a Rails process) and falls back to your `Gemfile.lock` for standalone CLI runs.
|
|
273
|
+
|
|
274
|
+
### SCSS audits
|
|
275
|
+
|
|
276
|
+
SCSS files (`.scss`) are routed to stylelint with `--customSyntax postcss-scss`. Install the parser globally:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
npm install -g postcss-scss
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`browsable doctor` flags `postcss-scss` as missing **only when** the project actually has SCSS files. Without it, SCSS files are still analyzed — but as plain CSS, so SCSS-specific syntax (nested selectors, variables) may produce parse warnings.
|
|
283
|
+
|
|
284
|
+
### What is not analyzed
|
|
285
|
+
|
|
286
|
+
- **CoffeeScript** (`*.coffee`) — no static-mode support.
|
|
287
|
+
- **ERB-templated JS/CSS** (`*.js.erb`, `*.css.erb`) — only the literal source is read; the ERB is not expanded.
|
|
288
|
+
- **Indented Sass** (`*.sass`) — discovered, but `postcss-scss` parses braced SCSS, not the indented dialect.
|
|
289
|
+
|
|
290
|
+
These are documented limitations of static mode. **Runtime mode** (below) sidesteps them entirely by reading the HTML, CSS, and JS that Rails actually renders during a test run — so any pipeline, preprocessor, or templating that ends up serving real content is covered.
|
|
291
|
+
|
|
259
292
|
## Runtime auditing (test-suite mode)
|
|
260
293
|
|
|
261
294
|
Static mode answers the question *“does my codebase satisfy a single browser-support target?”*. Runtime mode answers a sharper one: *“for every endpoint in my app, does the HTML it actually renders satisfy that endpoint's policy?”* It does this without trying to build a static asset → endpoint graph — instead, it lets Rails itself say what each endpoint renders during a test run, and audits *that*.
|
|
@@ -10,13 +10,26 @@ module Browsable
|
|
|
10
10
|
# project's target. browsable supplies the config; stylelint (and its
|
|
11
11
|
# bundled caniuse data) does the actual compatibility reasoning.
|
|
12
12
|
class CSS < Base
|
|
13
|
+
# Extensions that stylelint cannot parse with its default PostCSS
|
|
14
|
+
# syntax. We route them through postcss-scss when any are present.
|
|
15
|
+
SCSS_EXTENSIONS = %w[.scss .sass].freeze
|
|
16
|
+
|
|
17
|
+
def self.scss_like?(file)
|
|
18
|
+
SCSS_EXTENSIONS.include?(File.extname(file).downcase)
|
|
19
|
+
end
|
|
20
|
+
|
|
13
21
|
def required_tools = ["stylelint"]
|
|
14
22
|
|
|
15
23
|
def analyze(files)
|
|
16
24
|
return [] if files.empty?
|
|
17
25
|
|
|
18
26
|
argv = ["stylelint", "--config", write_stylelintrc,
|
|
19
|
-
"--formatter", "json"
|
|
27
|
+
"--formatter", "json"]
|
|
28
|
+
# SCSS is a strict superset of CSS for the constructs we audit; running
|
|
29
|
+
# plain .css through postcss-scss is safe, so one invocation covers
|
|
30
|
+
# mixed inputs as long as any SCSS-like file is present.
|
|
31
|
+
argv.push("--customSyntax", "postcss-scss") if files.any? { |f| self.class.scss_like?(f) }
|
|
32
|
+
argv.concat(files)
|
|
20
33
|
parse(shell_out(argv, dry_run_key: "BROWSABLE_DRY_RUN_CSS"))
|
|
21
34
|
end
|
|
22
35
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Browsable
|
|
4
|
+
# Identifies which Rails asset pipeline (Propshaft, Sprockets, both, or
|
|
5
|
+
# neither) is in use for a project. Reported in the audit header so the user
|
|
6
|
+
# sees at a glance which pipeline browsable inferred — and surfaced in the
|
|
7
|
+
# JSON output so editor integrations and CI can branch on it.
|
|
8
|
+
#
|
|
9
|
+
# Detection prefers a live `defined?` check (set when running inside the host
|
|
10
|
+
# Rails process, e.g. via the railtie or a rake task). Standalone CLI runs
|
|
11
|
+
# never load the host app, so the fallback inspects the project's
|
|
12
|
+
# Gemfile.lock — the canonical record of which asset-pipeline gem the app
|
|
13
|
+
# actually uses.
|
|
14
|
+
class AssetPipeline
|
|
15
|
+
PROPSHAFT = "propshaft"
|
|
16
|
+
SPROCKETS = "sprockets"
|
|
17
|
+
BOTH = "sprockets+propshaft"
|
|
18
|
+
NONE = "none"
|
|
19
|
+
|
|
20
|
+
# Build a pipeline descriptor for the project at `root`.
|
|
21
|
+
def self.detect(root:)
|
|
22
|
+
new(root: root)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attr_reader :root
|
|
26
|
+
|
|
27
|
+
def initialize(root:)
|
|
28
|
+
@root = File.expand_path(root)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# One of PROPSHAFT, SPROCKETS, BOTH, NONE.
|
|
32
|
+
def name
|
|
33
|
+
@name ||= identify
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def propshaft? = name == PROPSHAFT || name == BOTH
|
|
37
|
+
def sprockets? = name == SPROCKETS || name == BOTH
|
|
38
|
+
def none? = name == NONE
|
|
39
|
+
|
|
40
|
+
# When both pipelines are loaded (typical during a Propshaft migration),
|
|
41
|
+
# prefer Sprockets-style discovery — its source tree is the broader
|
|
42
|
+
# superset, so its globs match everything Propshaft would have found too.
|
|
43
|
+
def prefer_sprockets_layout? = sprockets?
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def identify
|
|
48
|
+
live_propshaft = defined?(::Propshaft) ? true : false
|
|
49
|
+
live_sprockets = defined?(::Sprockets) ? true : false
|
|
50
|
+
|
|
51
|
+
# Live `defined?` is authoritative — the host app actually loaded these.
|
|
52
|
+
return BOTH if live_propshaft && live_sprockets
|
|
53
|
+
return PROPSHAFT if live_propshaft
|
|
54
|
+
return SPROCKETS if live_sprockets
|
|
55
|
+
|
|
56
|
+
# Standalone CLI mode: fall back to what the Gemfile.lock declares.
|
|
57
|
+
lock_propshaft = gemfile_lock_mentions?("propshaft")
|
|
58
|
+
lock_sprockets = gemfile_lock_mentions?("sprockets-rails") ||
|
|
59
|
+
gemfile_lock_mentions?("sprockets")
|
|
60
|
+
|
|
61
|
+
return BOTH if lock_propshaft && lock_sprockets
|
|
62
|
+
return PROPSHAFT if lock_propshaft
|
|
63
|
+
return SPROCKETS if lock_sprockets
|
|
64
|
+
|
|
65
|
+
NONE
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def gemfile_lock_mentions?(gem_name)
|
|
69
|
+
path = File.join(root, "Gemfile.lock")
|
|
70
|
+
return false unless File.file?(path)
|
|
71
|
+
|
|
72
|
+
# `bundle` indents direct gem entries with four spaces; transitive deps
|
|
73
|
+
# appear under DEPENDENCIES with two. Either form counts as "in use".
|
|
74
|
+
File.read(path).match?(/^\s+#{Regexp.escape(gem_name)}\b/)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/browsable/cli.rb
CHANGED
|
@@ -61,7 +61,7 @@ module Browsable
|
|
|
61
61
|
option :fix, type: :boolean, default: false,
|
|
62
62
|
desc: "Attempt to install missing dependencies via brew/npm"
|
|
63
63
|
def doctor
|
|
64
|
-
doc = Doctor.new
|
|
64
|
+
doc = Doctor.new(root: Dir.pwd)
|
|
65
65
|
puts doc.render(color: color?)
|
|
66
66
|
|
|
67
67
|
if options[:fix] && !doc.ok?
|
|
@@ -172,12 +172,15 @@ module Browsable
|
|
|
172
172
|
# --- pipeline ------------------------------------------------------------
|
|
173
173
|
|
|
174
174
|
def run_audit(root:, config:, target:, file_list: nil)
|
|
175
|
-
|
|
175
|
+
doctor = Doctor.new(root: root)
|
|
176
|
+
available = doctor.available_kinds
|
|
176
177
|
skips = []
|
|
177
178
|
files_by_kind = file_list ? route_files(file_list) : discover_files(root: root, config: config)
|
|
178
179
|
|
|
179
180
|
collect_importmap(root: root, config: config, files_by_kind: files_by_kind, skips: skips) if file_list.nil?
|
|
180
181
|
|
|
182
|
+
check_scss_tooling!(doctor: doctor, files_by_kind: files_by_kind, skips: skips)
|
|
183
|
+
|
|
181
184
|
findings = []
|
|
182
185
|
ANALYZERS.each do |kind, analyzer_class|
|
|
183
186
|
files = files_by_kind[kind] || []
|
|
@@ -212,7 +215,22 @@ module Browsable
|
|
|
212
215
|
policies = file_list ? [] : PolicyScanner.call(root)
|
|
213
216
|
|
|
214
217
|
Report.new(findings: findings, skips: skips, notes: notes, policies: policies,
|
|
215
|
-
target: target, root: root, config_file: config.config_file
|
|
218
|
+
target: target, root: root, config_file: config.config_file,
|
|
219
|
+
pipeline: AssetPipeline.detect(root: root).name)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Warn (and skip the SCSS subset) when SCSS files were discovered but
|
|
223
|
+
# postcss-scss — the syntax stylelint needs to parse them — is missing.
|
|
224
|
+
def check_scss_tooling!(doctor:, files_by_kind:, skips:)
|
|
225
|
+
css_files = files_by_kind[:css] || []
|
|
226
|
+
return unless css_files.any? { |file| Analyzers::CSS.scss_like?(file) }
|
|
227
|
+
return if doctor.postcss_scss_installed?
|
|
228
|
+
|
|
229
|
+
skips << Report::Skip.new(
|
|
230
|
+
kind: :scss,
|
|
231
|
+
reason: "postcss-scss not found — SCSS files will be analyzed as plain CSS. " \
|
|
232
|
+
"Run `browsable doctor` for setup instructions."
|
|
233
|
+
)
|
|
216
234
|
end
|
|
217
235
|
|
|
218
236
|
def collect_importmap(root:, config:, files_by_kind:, skips:)
|
|
@@ -253,10 +271,10 @@ module Browsable
|
|
|
253
271
|
buckets = { css: [], erb: [], html: [], js: [] }
|
|
254
272
|
files.each do |file|
|
|
255
273
|
case File.extname(file).downcase
|
|
256
|
-
when ".css", ".scss" then buckets[:css] << file
|
|
257
|
-
when ".js", ".mjs"
|
|
258
|
-
when ".erb"
|
|
259
|
-
when ".html", ".htm"
|
|
274
|
+
when ".css", ".scss", ".sass" then buckets[:css] << file
|
|
275
|
+
when ".js", ".mjs" then buckets[:js] << file
|
|
276
|
+
when ".erb" then buckets[:erb] << file
|
|
277
|
+
when ".html", ".htm" then buckets[:html] << file
|
|
260
278
|
end
|
|
261
279
|
# TODO(v0.2): route Ruby view components (app/components/**/*.rb).
|
|
262
280
|
end
|
data/lib/browsable/config.rb
CHANGED
|
@@ -20,11 +20,16 @@ module Browsable
|
|
|
20
20
|
"manual_query" => "defaults"
|
|
21
21
|
},
|
|
22
22
|
"sources" => {
|
|
23
|
-
"stylesheets" => ["app/assets/stylesheets/**/*.{css,scss}"],
|
|
23
|
+
"stylesheets" => ["app/assets/stylesheets/**/*.{css,scss,sass}"],
|
|
24
24
|
"builds" => ["app/assets/builds/**/*.css"],
|
|
25
25
|
"views" => ["app/views/**/*.{html.erb,turbo_stream.erb}",
|
|
26
26
|
"app/components/**/*.{rb,html.erb}"],
|
|
27
|
-
|
|
27
|
+
# `app/javascript` is the Propshaft/importmap convention;
|
|
28
|
+
# `app/assets/javascripts` is the Sprockets convention. Globbing both
|
|
29
|
+
# is harmless on a Propshaft-only app (no files match) and lets the
|
|
30
|
+
# CLI work on Sprockets apps with zero configuration.
|
|
31
|
+
"javascript" => ["app/javascript/**/*.{js,mjs}",
|
|
32
|
+
"app/assets/javascripts/**/*.{js,mjs}"],
|
|
28
33
|
"importmap" => true,
|
|
29
34
|
"public" => ["public/**/*.{html,css,js}"],
|
|
30
35
|
"custom" => []
|
data/lib/browsable/doctor.rb
CHANGED
|
@@ -42,12 +42,25 @@ module Browsable
|
|
|
42
42
|
Tool.new(key: :eslint_plugin_compat, label: "eslint-plugin-compat", binary: nil,
|
|
43
43
|
npm_package: "eslint-plugin-compat",
|
|
44
44
|
purpose: "the eslint plugin that performs the JS compat checks",
|
|
45
|
-
enables: %i[js], required: true)
|
|
45
|
+
enables: %i[js], required: true),
|
|
46
|
+
Tool.new(key: :postcss_scss, label: "postcss-scss", binary: nil,
|
|
47
|
+
npm_package: "postcss-scss",
|
|
48
|
+
purpose: "lets stylelint parse SCSS sources (Sprockets apps)",
|
|
49
|
+
enables: %i[scss], required: false)
|
|
46
50
|
].freeze
|
|
47
51
|
|
|
48
52
|
# Analyzer kinds that need no external tooling at all.
|
|
49
53
|
ALWAYS_AVAILABLE = %i[erb html].freeze
|
|
50
54
|
|
|
55
|
+
# @param root [String, nil] the project root. When provided, optional tools
|
|
56
|
+
# (e.g. postcss-scss) are only flagged as missing if the project actually
|
|
57
|
+
# has files that need them.
|
|
58
|
+
def initialize(root: nil)
|
|
59
|
+
@root = root && File.expand_path(root)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
attr_reader :root
|
|
63
|
+
|
|
51
64
|
def statuses
|
|
52
65
|
@statuses ||= TOOLS.map do |tool|
|
|
53
66
|
present = installed?(tool)
|
|
@@ -64,6 +77,33 @@ module Browsable
|
|
|
64
77
|
statuses.reject(&:installed?).map(&:tool)
|
|
65
78
|
end
|
|
66
79
|
|
|
80
|
+
def postcss_scss_installed?
|
|
81
|
+
tool = TOOLS.find { |t| t.key == :postcss_scss }
|
|
82
|
+
installed?(tool)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Whether the project at `root` actually has files that need this tool.
|
|
86
|
+
# For unconditional tools (e.g. node, stylelint) this is always true; for
|
|
87
|
+
# optional tools (e.g. postcss-scss) it depends on what's on disk.
|
|
88
|
+
def needs_tool?(tool)
|
|
89
|
+
return true if tool.required
|
|
90
|
+
return true if tool.enables.empty? # tools that enable nothing are housekeeping
|
|
91
|
+
return true unless root # no project context: assume needed
|
|
92
|
+
|
|
93
|
+
tool.enables.all? { |kind| project_has_kind?(kind) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Optional tools that *would* be needed by this project but aren't installed.
|
|
97
|
+
# Used by the audit CLI to surface targeted skips (e.g. postcss-scss missing
|
|
98
|
+
# only when the project has SCSS files).
|
|
99
|
+
def needed_optional_missing
|
|
100
|
+
missing.select do |tool|
|
|
101
|
+
next false if tool.required
|
|
102
|
+
|
|
103
|
+
needs_tool?(tool)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
67
107
|
# Which analyzer kinds can actually run on this machine right now.
|
|
68
108
|
def available_kinds
|
|
69
109
|
# In dry-run mode the external tools are never invoked, so treat them all
|
|
@@ -84,8 +124,9 @@ module Browsable
|
|
|
84
124
|
lines = [pastel.bold("browsable doctor — system dependencies"), ""]
|
|
85
125
|
|
|
86
126
|
statuses.each do |status|
|
|
87
|
-
mark =
|
|
88
|
-
|
|
127
|
+
mark = render_mark(pastel, status)
|
|
128
|
+
suffix = render_suffix(pastel, status)
|
|
129
|
+
lines << " #{mark} #{pastel.bold(status.tool.label)} — #{status.tool.purpose}#{suffix}"
|
|
89
130
|
lines << pastel.dim(" #{status.detail}") if status.detail
|
|
90
131
|
end
|
|
91
132
|
|
|
@@ -130,8 +171,12 @@ module Browsable
|
|
|
130
171
|
|
|
131
172
|
private
|
|
132
173
|
|
|
174
|
+
# Install commands cover every required tool plus any optional tool the
|
|
175
|
+
# current project actually needs (e.g. postcss-scss when SCSS is present).
|
|
133
176
|
def install_commands
|
|
134
|
-
missing.
|
|
177
|
+
missing.select { |tool| tool.required || needs_tool?(tool) }
|
|
178
|
+
.map { |tool| install_command(tool) }
|
|
179
|
+
.uniq
|
|
135
180
|
end
|
|
136
181
|
|
|
137
182
|
def install_command(tool)
|
|
@@ -146,11 +191,40 @@ module Browsable
|
|
|
146
191
|
def detail_for(tool, present)
|
|
147
192
|
if present
|
|
148
193
|
tool.binary? ? (tool_version(tool) || "installed") : "installed"
|
|
149
|
-
|
|
194
|
+
elsif tool.required || needs_tool?(tool)
|
|
150
195
|
"not found — #{install_command(tool)}"
|
|
196
|
+
else
|
|
197
|
+
"not installed (not needed for this project)"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def project_has_kind?(kind)
|
|
202
|
+
case kind
|
|
203
|
+
when :scss
|
|
204
|
+
Dir.glob(File.join(root, "app/assets/stylesheets/**/*.{scss,sass}"),
|
|
205
|
+
File::FNM_EXTGLOB).any?
|
|
206
|
+
else
|
|
207
|
+
true
|
|
151
208
|
end
|
|
152
209
|
end
|
|
153
210
|
|
|
211
|
+
# Optional tools that are missing-but-needed get the same red ✗ as required
|
|
212
|
+
# tools. Optional tools the project doesn't need are shown as a dim "—".
|
|
213
|
+
def render_mark(pastel, status)
|
|
214
|
+
return pastel.green("✓") if status.installed?
|
|
215
|
+
return pastel.red("✗") if status.tool.required || needs_tool?(status.tool)
|
|
216
|
+
|
|
217
|
+
pastel.dim("—")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def render_suffix(pastel, status)
|
|
221
|
+
return "" if status.tool.required
|
|
222
|
+
return pastel.dim(" (optional)") if status.installed?
|
|
223
|
+
return pastel.yellow(" (optional, but needed for this project)") if needs_tool?(status.tool)
|
|
224
|
+
|
|
225
|
+
pastel.dim(" (optional)")
|
|
226
|
+
end
|
|
227
|
+
|
|
154
228
|
def installed?(tool)
|
|
155
229
|
@installed_cache ||= {}
|
|
156
230
|
@installed_cache.fetch(tool.key) do
|
|
@@ -33,6 +33,7 @@ module Browsable
|
|
|
33
33
|
lines << pastel.dim("target: #{target.query} (#{browsers})")
|
|
34
34
|
end
|
|
35
35
|
lines << pastel.dim("config: #{report.config_file || 'none (no config file)'}")
|
|
36
|
+
lines << pastel.dim("pipeline: #{report.pipeline}") if report.pipeline
|
|
36
37
|
lines.join("\n") + "\n"
|
|
37
38
|
end
|
|
38
39
|
|
data/lib/browsable/replay.rb
CHANGED
data/lib/browsable/report.rb
CHANGED
|
@@ -15,14 +15,16 @@ module Browsable
|
|
|
15
15
|
# `bumps` maps each raised browser to { from:, to: }.
|
|
16
16
|
Suggestion = Data.define(:line, :bumps)
|
|
17
17
|
|
|
18
|
-
attr_reader :findings, :skips, :notes, :policies, :target, :root, :config_file
|
|
18
|
+
attr_reader :findings, :skips, :notes, :policies, :target, :root, :config_file, :pipeline
|
|
19
19
|
|
|
20
20
|
# @param notes [Array<String>] caveats about the run itself (e.g. a target
|
|
21
21
|
# that could not be inferred) — distinct from per-file findings.
|
|
22
22
|
# @param policies [Array<PolicyScanner::Policy>] every allow_browser callsite
|
|
23
23
|
# discovered across the app's controllers — the policy landscape.
|
|
24
|
+
# @param pipeline [String, nil] the detected asset pipeline
|
|
25
|
+
# ("propshaft", "sprockets", "sprockets+propshaft", or "none").
|
|
24
26
|
def initialize(findings: [], skips: [], notes: [], policies: [],
|
|
25
|
-
target: nil, root: nil, config_file: nil)
|
|
27
|
+
target: nil, root: nil, config_file: nil, pipeline: nil)
|
|
26
28
|
@findings = findings
|
|
27
29
|
@skips = skips
|
|
28
30
|
@notes = notes
|
|
@@ -30,6 +32,7 @@ module Browsable
|
|
|
30
32
|
@target = target
|
|
31
33
|
@root = root
|
|
32
34
|
@config_file = config_file
|
|
35
|
+
@pipeline = pipeline
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def errors = findings.select(&:error?)
|
|
@@ -74,6 +77,7 @@ module Browsable
|
|
|
74
77
|
def as_json
|
|
75
78
|
{
|
|
76
79
|
target: target&.as_json,
|
|
80
|
+
pipeline: pipeline,
|
|
77
81
|
notes: notes,
|
|
78
82
|
summary: {
|
|
79
83
|
errors: errors.size,
|
data/lib/browsable/version.rb
CHANGED
|
@@ -25,10 +25,12 @@ target:
|
|
|
25
25
|
|
|
26
26
|
# sources:
|
|
27
27
|
# # Globs are relative to the project root. The values shown are the defaults.
|
|
28
|
-
# stylesheets: ["app/assets/stylesheets/**/*.{css,scss}"]
|
|
28
|
+
# stylesheets: ["app/assets/stylesheets/**/*.{css,scss,sass}"]
|
|
29
29
|
# builds: ["app/assets/builds/**/*.css"]
|
|
30
30
|
# views: ["app/views/**/*.{html.erb,turbo_stream.erb}", "app/components/**/*.{rb,html.erb}"]
|
|
31
|
-
#
|
|
31
|
+
# # Both Propshaft/importmap (app/javascript) and Sprockets
|
|
32
|
+
# # (app/assets/javascripts) layouts are discovered by default.
|
|
33
|
+
# javascript: ["app/javascript/**/*.{js,mjs}", "app/assets/javascripts/**/*.{js,mjs}"]
|
|
32
34
|
# importmap: true # resolve config/importmap.rb pins and audit pinned JS
|
|
33
35
|
# public: ["public/**/*.{html,css,js}"]
|
|
34
36
|
# custom: [] # extra globs to fold into the audit
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: browsable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Hood
|
|
@@ -174,6 +174,7 @@ files:
|
|
|
174
174
|
- lib/browsable/analyzers/erb.rb
|
|
175
175
|
- lib/browsable/analyzers/html.rb
|
|
176
176
|
- lib/browsable/analyzers/javascript.rb
|
|
177
|
+
- lib/browsable/asset_pipeline.rb
|
|
177
178
|
- lib/browsable/asset_resolver.rb
|
|
178
179
|
- lib/browsable/audit_log.rb
|
|
179
180
|
- lib/browsable/cli.rb
|