browsable 0.1.0 → 0.2.0.pre.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 +34 -0
- data/README.md +340 -147
- data/bin/benchmark-asset-resolution +91 -0
- data/lib/browsable/asset_resolver.rb +200 -0
- data/lib/browsable/audit_log.rb +97 -0
- data/lib/browsable/cli.rb +24 -0
- data/lib/browsable/drivers/minitest.rb +100 -0
- data/lib/browsable/drivers/rspec.rb +117 -0
- data/lib/browsable/html_extractor.rb +114 -0
- data/lib/browsable/middleware.rb +124 -0
- data/lib/browsable/minitest.rb +25 -0
- data/lib/browsable/policy.rb +63 -0
- data/lib/browsable/policy_resolver.rb +153 -0
- data/lib/browsable/replay.rb +115 -0
- data/lib/browsable/rspec.rb +28 -0
- data/lib/browsable/test_report.rb +329 -0
- data/lib/browsable/version.rb +1 -1
- data/lib/browsable.rb +8 -4
- metadata +57 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9484cb575cb9dec80631552a5fad005ad12c8cec353c61d139d9a299072a4f05
|
|
4
|
+
data.tar.gz: 5c1c08df4256e57348aa46c381e81a2bc628cc2bf4cd3ba8a808eef56c67b425
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97f7dff9786a2b8d697e7043c8a8f61000ef5a44e3e7826e2c9e5de1639b370a7e3908bb8581813dbefdfeda304bfb561f232d287f4c82adaae58782b36d6f90
|
|
7
|
+
data.tar.gz: d2d0002312062a738b2179126c32bf958a9997c722d7ec46903de6b9e8af535a2ebe2847c09f56873d330a75e0d0c9fedabc2d48bbbbb71731b33c3223d6667b
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Added — v0.2: Runtime response auditing
|
|
11
|
+
|
|
12
|
+
- **Runtime mode.** A Rack middleware (`Browsable::Middleware`) observes HTML
|
|
13
|
+
responses during a test run, records the controller#action, the effective
|
|
14
|
+
`allow_browser` policy, and every asset the response loaded, into a
|
|
15
|
+
thread-safe `Browsable::AuditLog`. Test-suite integration is opt-in via a
|
|
16
|
+
single `require`: `browsable/rspec` or `browsable/minitest`.
|
|
17
|
+
- **End-of-suite analysis (`Browsable::TestReport`).** The middleware records;
|
|
18
|
+
it never analyzes. At suite end, stylelint and eslint are invoked **once**
|
|
19
|
+
each over the deduplicated union of every asset loaded across the suite —
|
|
20
|
+
not per request. A 500-spec suite that loads 10 unique CSS files spawns
|
|
21
|
+
exactly one stylelint process.
|
|
22
|
+
- **Per-endpoint policy resolution (`Browsable::PolicyResolver`).** Walks the
|
|
23
|
+
controller's ancestor chain, applies each `allow_browser` call's
|
|
24
|
+
`only:`/`except:` filter to the action, and the last matching call wins —
|
|
25
|
+
matching Rails' filter-callback semantics.
|
|
26
|
+
- **Asset URL → on-disk path resolution (`Browsable::AssetResolver`).** Strips
|
|
27
|
+
digests, honors the configured asset host, and walks Propshaft and Sprockets
|
|
28
|
+
search paths with a public/ fallback. The ≥95% resolution bar is enforced by
|
|
29
|
+
the new `bin/benchmark-asset-resolution` script.
|
|
30
|
+
- **`browsable replay PATH`** — re-renders a JSON audit dump through any
|
|
31
|
+
formatter, suitable for emitting GitHub annotations in CI from a saved
|
|
32
|
+
test-suite report.
|
|
33
|
+
- **Dependencies.** Adds `nokogiri` (response HTML parsing), `concurrent-ruby`
|
|
34
|
+
(thread-safe `AuditLog`), and `rack` (Rack body handling) as runtime gem
|
|
35
|
+
dependencies. **The user's Rails app still has no `package.json` and no
|
|
36
|
+
`node_modules`.** Runtime mode shells out to the same globally-installed
|
|
37
|
+
`stylelint` and `eslint` as static mode.
|
|
38
|
+
|
|
39
|
+
### Unchanged
|
|
40
|
+
|
|
41
|
+
- Static `browsable audit` and the v0.1 CLI surface are fully backwards
|
|
42
|
+
compatible. Runtime mode is purely additive.
|
|
43
|
+
|
|
10
44
|
## [0.1.0]
|
|
11
45
|
|
|
12
46
|
- Initial release.
|
data/README.md
CHANGED
|
@@ -1,32 +1,56 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Browsable
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
reports which browsers can actually render and run it — then compares that
|
|
7
|
-
against the project's declared `allow_browser` policy.
|
|
5
|
+
**Rails-aware browser-compatibility auditing for your frontend.**
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
*declaring* which browsers you allow, `browsable` tells you which browsers your
|
|
11
|
-
code is actually **browsable by**.
|
|
7
|
+
Find out which browsers your Rails app is actually *browsable by* — before your users do.
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
[](https://rubygems.org/gems/browsable)
|
|
10
|
+
[](https://github.com/romanhood/browsable/actions/workflows/ci.yml)
|
|
11
|
+
[](../LICENSE)
|
|
12
|
+
[](https://www.ruby-lang.org/)
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
</div>
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
Browsable audits a Rails application's CSS, HTML, ERB, and JavaScript and reports which browsers can actually render and run it — then compares the answer against the `allow_browser` policy you've declared.
|
|
19
|
+
|
|
20
|
+
The name is a play on Rails 8's `allow_browser` controller API. Instead of *declaring* which browsers you allow, `browsable` tells you which browsers your code is actually browsable by.
|
|
21
|
+
|
|
22
|
+
> 📦 This is the core gem of the [`browsable` monorepo][monorepo].
|
|
23
|
+
> See also [`browsable-lsp`][lsp] for editor diagnostics and [`browsable.nvim`][nvim] for Neovim.
|
|
24
|
+
|
|
25
|
+
## Table of contents
|
|
26
|
+
|
|
27
|
+
- [Why Browsable?](#why-browsable)
|
|
28
|
+
- [Installation](#installation)
|
|
29
|
+
- [Quick start](#quick-start)
|
|
30
|
+
- [System dependencies](#system-dependencies)
|
|
31
|
+
- [CLI reference](#cli-reference)
|
|
32
|
+
- [Configuration](#configuration)
|
|
33
|
+
- [How it works](#how-it-works)
|
|
34
|
+
- [Runtime auditing (test-suite mode)](#runtime-auditing-test-suite-mode)
|
|
35
|
+
- [Per-controller policies](#per-controller-and-per-action-policies)
|
|
36
|
+
- [Suggested policy fixes](#suggested-allow_browser-fix)
|
|
37
|
+
- [Rake tasks](#rake-tasks)
|
|
38
|
+
- [Contributing](#contributing)
|
|
39
|
+
- [License](#license)
|
|
40
|
+
|
|
41
|
+
## Why Browsable?
|
|
42
|
+
|
|
43
|
+
Rails 8 made browser support a first-class concern with `allow_browser`. But the framework has no opinion on whether your CSS actually works in the browsers you allowed. You can declare `allow_browser :modern` and silently ship `:has()` selectors that break in Safari 15. There was no tool that closed that loop — until now.
|
|
44
|
+
|
|
45
|
+
Browsable closes it by:
|
|
46
|
+
|
|
47
|
+
- 🔍 **Reading your `allow_browser` policy** straight from `ApplicationController`
|
|
48
|
+
- 🎯 **Translating it** into a precise browserslist query
|
|
49
|
+
- 📂 **Discovering** your stylesheets, views, JavaScript, and importmap pins
|
|
50
|
+
- ✅ **Auditing each** against best-in-class compat databases (MDN BCD, caniuse)
|
|
51
|
+
- 📋 **Reporting** by file, with exact lines and suggested fixes
|
|
52
|
+
|
|
53
|
+
No `package.json`. No `node_modules`. No build-system pollution in your Rails repo.
|
|
30
54
|
|
|
31
55
|
## Installation
|
|
32
56
|
|
|
@@ -44,49 +68,84 @@ Then `bundle install`. Or install it standalone:
|
|
|
44
68
|
gem install browsable
|
|
45
69
|
```
|
|
46
70
|
|
|
47
|
-
|
|
71
|
+
> 💡 **Heads up:** Browsable shells out to `stylelint` and `eslint` for CSS and JS analysis. These live globally on your machine — *not* in your Rails repo. Run `browsable doctor` to check and install them.
|
|
48
72
|
|
|
49
|
-
|
|
50
|
-
need). Those are *not* gem dependencies — they live globally on your machine.
|
|
51
|
-
Check what you have:
|
|
73
|
+
## Quick start
|
|
52
74
|
|
|
53
75
|
```bash
|
|
54
|
-
bundle exec browsable
|
|
76
|
+
bundle exec browsable audit
|
|
55
77
|
```
|
|
56
78
|
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
That's it. With zero configuration, Browsable will:
|
|
80
|
+
|
|
81
|
+
1. **Read** `ApplicationController`'s `allow_browser` policy to learn your target
|
|
82
|
+
2. **Discover** your stylesheets, views, JavaScript, and importmap pins
|
|
83
|
+
3. **Audit** each against that target
|
|
84
|
+
4. **Report** the findings, grouped by file
|
|
85
|
+
|
|
86
|
+
### Example output
|
|
59
87
|
|
|
60
|
-
```bash
|
|
61
|
-
bundle exec browsable doctor --fix # installs missing tools via brew / npm
|
|
62
88
|
```
|
|
89
|
+
$ bundle exec browsable audit
|
|
63
90
|
|
|
64
|
-
|
|
65
|
-
|
|
91
|
+
✓ Target inferred from ApplicationController.allow_browser :modern
|
|
92
|
+
→ chrome 120, edge 120, firefox 121, safari 17.2, opera 106
|
|
66
93
|
|
|
67
|
-
|
|
94
|
+
⚠ app/assets/stylesheets/cards.css
|
|
95
|
+
42:3 :has() selector requires Safari 15.4+ (policy allows 17.2 ✓)
|
|
96
|
+
87:5 @container query requires Firefox 110+ (policy allows 121 ✓)
|
|
97
|
+
|
|
98
|
+
✗ app/views/legacy/embed.html.erb
|
|
99
|
+
14:22 <dialog> element requires Safari 15.4+, but the LegacyController
|
|
100
|
+
policy allows Safari 12.0
|
|
101
|
+
|
|
102
|
+
Browser policies (2 found)
|
|
103
|
+
ApplicationController :modern
|
|
104
|
+
LegacyController { safari: 12, chrome: 60 } (only: embed)
|
|
105
|
+
|
|
106
|
+
1 error, 0 warnings — exit 1
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## System dependencies
|
|
110
|
+
|
|
111
|
+
Browsable shells out to a few external tools that live globally on your machine:
|
|
112
|
+
|
|
113
|
+
| Tool | Purpose | Required? |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| `node` | JavaScript runtime for `stylelint` & `eslint` | Yes |
|
|
116
|
+
| `stylelint` | CSS compatibility analysis | Yes (CSS audits) |
|
|
117
|
+
| `eslint` + `eslint-plugin-compat` | JavaScript compatibility analysis | Yes (JS audits) |
|
|
118
|
+
| `browserslist` | Live resolution of `defaults` queries | Optional |
|
|
119
|
+
| `herb` | ERB parsing | Bundled (gem dep) |
|
|
120
|
+
|
|
121
|
+
### The `doctor` workflow
|
|
68
122
|
|
|
69
123
|
```bash
|
|
70
|
-
bundle exec browsable
|
|
124
|
+
bundle exec browsable doctor
|
|
71
125
|
```
|
|
72
126
|
|
|
73
|
-
|
|
127
|
+
For each missing tool, `doctor` prints the exact command to install it. Or let it do the work:
|
|
74
128
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
129
|
+
```bash
|
|
130
|
+
bundle exec browsable doctor --fix
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
This installs missing tools via `brew` or `npm` — opt-in, never automatic.
|
|
79
134
|
|
|
80
135
|
## CLI reference
|
|
81
136
|
|
|
137
|
+
### Commands
|
|
138
|
+
|
|
82
139
|
| Command | Purpose |
|
|
83
140
|
| --- | --- |
|
|
84
|
-
| `browsable`
|
|
141
|
+
| `browsable` *(or `browsable audit`)* | Full project audit |
|
|
142
|
+
| `browsable audit [PATH]` | Audit a specific directory |
|
|
143
|
+
| `browsable check FILE [FILE...]` | Audit specific files *(used by editors)* |
|
|
85
144
|
| `browsable doctor` | Check system dependencies |
|
|
86
|
-
| `browsable doctor --fix` | Install missing dependencies
|
|
87
|
-
| `browsable check FILE [FILE...]` | Audit specific files (used by editors) |
|
|
145
|
+
| `browsable doctor --fix` | Install missing dependencies |
|
|
88
146
|
| `browsable target [PATH]` | Show the inferred browser-support target |
|
|
89
|
-
| `browsable
|
|
147
|
+
| `browsable replay PATH` | Reformat a JSON audit dump *(test-suite mode → GitHub annotations)* |
|
|
148
|
+
| `browsable init` | Generate `.browsable.yml` *(non-Rails projects)* |
|
|
90
149
|
| `browsable version` | Print the version |
|
|
91
150
|
|
|
92
151
|
### Flags
|
|
@@ -94,147 +153,281 @@ That's it. With no configuration, `browsable`:
|
|
|
94
153
|
| Flag | Effect |
|
|
95
154
|
| --- | --- |
|
|
96
155
|
| `--target QUERY` | Override the inferred browserslist query |
|
|
97
|
-
| `--json` | Emit findings as JSON (shortcut for `--format json`) |
|
|
156
|
+
| `--json` | Emit findings as JSON *(shortcut for `--format json`)* |
|
|
98
157
|
| `--format human\|json\|github` | Choose the output formatter |
|
|
99
|
-
| `--no-build` | Scan only what
|
|
100
|
-
| `--include GLOB` | Add a path glob
|
|
101
|
-
| `--exclude GLOB` | Exclude a path glob (repeatable) |
|
|
158
|
+
| `--no-build` | Scan only what's on disk *(Browsable never builds assets itself)* |
|
|
159
|
+
| `--include GLOB` | Add a path glob *(repeatable)* |
|
|
160
|
+
| `--exclude GLOB` | Exclude a path glob *(repeatable)* |
|
|
102
161
|
| `--fail-on warning\|error` | Exit-code policy for CI |
|
|
103
162
|
| `--config PATH` | Override the config file location |
|
|
104
163
|
|
|
105
|
-
The `--json` output is the universal interface
|
|
106
|
-
MCP server) consume exactly that structure. The human and GitHub formatters are
|
|
107
|
-
just alternate presentations of the same data.
|
|
164
|
+
> 💡 The `--json` output is the universal interface. The LSP server and any future MCP server consume that exact structure. The `human` and `github` formatters are just alternate presentations of the same data.
|
|
108
165
|
|
|
109
|
-
##
|
|
166
|
+
## Configuration
|
|
167
|
+
|
|
168
|
+
**Browsable needs no config file.** Configuration is for overrides only.
|
|
169
|
+
|
|
170
|
+
When a file is present, it's discovered in this order:
|
|
171
|
+
|
|
172
|
+
1. The path passed to `--config`
|
|
173
|
+
2. `config/browsable.yml` *(preferred in Rails apps)*
|
|
174
|
+
3. `.browsable.yml` in the working directory
|
|
175
|
+
|
|
176
|
+
Resolution precedence (highest wins):
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
CLI flags → config file → inferred Rails config → gem defaults
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Generating a config file
|
|
110
183
|
|
|
111
184
|
```bash
|
|
112
185
|
rails g browsable:install
|
|
113
186
|
```
|
|
114
187
|
|
|
115
|
-
This writes a fully-commented `config/browsable.yml` — every option present,
|
|
116
|
-
|
|
117
|
-
|
|
188
|
+
This writes a fully-commented `config/browsable.yml` — every option present, commented out, set to its default. It's a self-documenting reference: uncomment a line to override it.
|
|
189
|
+
|
|
190
|
+
| Flag | Effect |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| `--minimal` | Section headers only, no option reference |
|
|
193
|
+
| `--target QUERY` | Pre-populate the target |
|
|
194
|
+
| `--force` | Overwrite an existing config |
|
|
118
195
|
|
|
119
196
|
Non-Rails projects use `browsable init`, which writes `.browsable.yml` instead.
|
|
120
197
|
|
|
121
|
-
##
|
|
198
|
+
## How it works
|
|
122
199
|
|
|
123
|
-
|
|
124
|
-
order:
|
|
200
|
+
### The inference chain
|
|
125
201
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
202
|
+
```
|
|
203
|
+
ApplicationController.allow_browser → Target
|
|
204
|
+
:modern chrome 120, safari 17.2, ...
|
|
205
|
+
│
|
|
206
|
+
▼
|
|
207
|
+
config/importmap.rb ─┐ Sources
|
|
208
|
+
app/assets/** ─┼─→ discovered files ─→ │
|
|
209
|
+
app/views/** ─┤ │
|
|
210
|
+
app/javascript/** ─┘ ▼
|
|
211
|
+
Analyzers
|
|
212
|
+
│
|
|
213
|
+
CSS → stylelint
|
|
214
|
+
ERB → Herb + MDN BCD
|
|
215
|
+
HTML → Herb + MDN BCD
|
|
216
|
+
JS → eslint + eslint-plugin-compat
|
|
217
|
+
│
|
|
218
|
+
▼
|
|
219
|
+
Report → Formatter
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Browsable's job is the **glue between Rails-land and browserslist-land**. It reads `allow_browser :modern`, expands it to concrete browser versions, configures stylelint and eslint with that target, and runs Herb against a bundled MDN browser-compat-data snapshot for ERB and HTML.
|
|
223
|
+
|
|
224
|
+
### Partial `allow_browser` policies
|
|
225
|
+
|
|
226
|
+
If your `allow_browser` policy is a hash that pins only some browsers — say `versions: { safari: 16.4, firefox: 121 }` — Rails leaves every browser you *don't* list allowed at any version. It only blocks a browser it was explicitly given a minimum (or `false`) for.
|
|
129
227
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
228
|
+
Browsable audits exactly the browsers you pinned and prints a note naming the rest. To audit against more, set an explicit `target:` in `config/browsable.yml`. The same note-and-fall-back-to-`defaults` behavior applies when Browsable can't resolve your policy statically.
|
|
229
|
+
|
|
230
|
+
### Where `defaults` comes from
|
|
133
231
|
|
|
134
|
-
|
|
232
|
+
When there's no `allow_browser` policy at all, Browsable audits against the [browserslist `defaults`][browserslist] query — the "reasonable broad support" baseline the wider frontend ecosystem uses.
|
|
233
|
+
|
|
234
|
+
- **With `browserslist` installed** *(`npm install -g browserslist`)*: resolved live from caniuse data
|
|
235
|
+
- **Without it**: a small built-in approximation, with a note saying so
|
|
236
|
+
|
|
237
|
+
Either way, these versions are *not* a Rails concept — Rails blocks nothing unless you call `allow_browser` — and they aren't derived from stylelint or eslint. For a precise, stable target, set `target:` in `config/browsable.yml`.
|
|
238
|
+
|
|
239
|
+
## Suggested `allow_browser` fix
|
|
240
|
+
|
|
241
|
+
When an audit finds errors that are purely a version conflict — your code needs a browser version newer than your policy permits — Browsable prints a ready-to-paste `allow_browser` line that raises *only* the offending browsers to the minimum those features require:
|
|
135
242
|
|
|
136
243
|
```
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
Report → Formatter
|
|
244
|
+
💡 Suggested allow_browser policy
|
|
245
|
+
|
|
246
|
+
allow_browser versions: {
|
|
247
|
+
chrome: 120,
|
|
248
|
+
edge: 120,
|
|
249
|
+
firefox: 125, # ← was 121
|
|
250
|
+
safari: 17.2,
|
|
251
|
+
opera: 106
|
|
252
|
+
}
|
|
147
253
|
```
|
|
148
254
|
|
|
149
|
-
`
|
|
150
|
-
`allow_browser :modern`, expands it to concrete browser versions, configures
|
|
151
|
-
stylelint/eslint with that target, and runs Herb against the bundled MDN
|
|
152
|
-
browser-compat-data snapshot for ERB/HTML.
|
|
255
|
+
It's a suggestion, not an instruction. Tightening the policy is one fix; changing the code (a fallback, a `@supports` rule) is another. **Browsable reports — you decide.**
|
|
153
256
|
|
|
154
|
-
|
|
257
|
+
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.
|
|
155
258
|
|
|
156
|
-
|
|
157
|
-
`versions: { safari: 16.4, firefox: 121 }` — Rails leaves every browser you
|
|
158
|
-
*don't* list allowed at **any** version (it only blocks a browser it was given a
|
|
159
|
-
minimum, or `false`, for). browsable audits exactly the browsers you pinned and
|
|
160
|
-
prints a note naming the rest. To audit against more, set an explicit `target:`
|
|
161
|
-
in `config/browsable.yml`. The same note-and-fall-back-to-`defaults` behaviour
|
|
162
|
-
applies when browsable cannot resolve your policy statically.
|
|
259
|
+
## Runtime auditing (test-suite mode)
|
|
163
260
|
|
|
164
|
-
|
|
261
|
+
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*.
|
|
262
|
+
|
|
263
|
+
Runtime mode uses **the same machine-level tools as static mode** — `node`, `stylelint`, `eslint`. No `package.json`, no `node_modules` in your Rails app. The middleware records during the suite; analysis happens **once**, at the end, with one stylelint and one eslint invocation regardless of how many request specs you ran.
|
|
165
264
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
265
|
+
### Adoption
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
# Gemfile
|
|
269
|
+
group :development, :test do
|
|
270
|
+
gem "browsable"
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
bundle install
|
|
276
|
+
bundle exec browsable doctor # one-time, installs stylelint / eslint if missing
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
# spec/rails_helper.rb (RSpec)
|
|
281
|
+
require "browsable/rspec"
|
|
282
|
+
|
|
283
|
+
# OR test/test_helper.rb (Minitest)
|
|
284
|
+
require "browsable/minitest"
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Then run your suite as you normally would:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
bundle exec rspec # or: bundle exec rails test
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
At end-of-suite Browsable prints a report grouped by `Controller#action`, with each finding evaluated against that endpoint's effective `allow_browser` policy.
|
|
294
|
+
|
|
295
|
+
### How it works
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
┌──────────────────────┐
|
|
299
|
+
│ Rack middleware │ per request: parse HTML, resolve asset URLs,
|
|
300
|
+
│ (records only) │ look up policy, push to AuditLog. NO subprocesses.
|
|
301
|
+
└──────────┬───────────┘
|
|
302
|
+
│
|
|
303
|
+
▼
|
|
304
|
+
┌──────────────────────┐
|
|
305
|
+
│ AuditLog │ thread-safe accumulator of
|
|
306
|
+
│ (in-memory) │ (endpoint, policy, html, asset_paths)
|
|
307
|
+
└──────────┬───────────┘
|
|
308
|
+
│ end of suite
|
|
309
|
+
▼
|
|
310
|
+
┌──────────────────────┐
|
|
311
|
+
│ TestReport │ deduplicated asset universe ➜
|
|
312
|
+
│ (one stylelint, │ stylelint × 1, eslint × 1
|
|
313
|
+
│ one eslint call) │ findings ➜ attributed back to endpoints
|
|
314
|
+
└──────────────────────┘
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
- **The middleware never analyzes.** Per-request work is a Nokogiri parse plus URL resolution — under a few milliseconds on a typical page.
|
|
318
|
+
- **The middleware never runs in production.** It raises at construction if `Rails.env.production?`.
|
|
319
|
+
- **Analysis is one batch.** A 500-spec suite that hits 50 unique HTML pages loading the same 10 CSS files spawns **two Node processes**, total — not 500.
|
|
320
|
+
- **Endpoint-level policies.** The `PolicyResolver` walks each controller's ancestor chain, applies each `allow_browser` call's `only:`/`except:` filter, and picks the last matching one — matching Rails' own filter-callback semantics.
|
|
321
|
+
|
|
322
|
+
### Configuration
|
|
323
|
+
|
|
324
|
+
The drivers ship with sensible defaults. Override per-suite:
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
Browsable::RSpec.configure do |c|
|
|
328
|
+
c.fail_on = :error # :error | :warning | :never
|
|
329
|
+
c.format = :human # :human | :json | :github
|
|
330
|
+
c.output = "tmp/browsable_report.json"
|
|
331
|
+
end
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
For CI: dump the report as JSON during the test run, then re-render it as GitHub annotations:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
bundle exec rspec
|
|
338
|
+
bundle exec browsable replay tmp/browsable_report.json --format github
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Example output
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
browsable audit
|
|
345
|
+
target: runtime-union (chrome 100, firefox 121, safari 17.2)
|
|
346
|
+
|
|
347
|
+
[response] PostsController#show
|
|
348
|
+
✗ 14:22 popover the 'popover' attribute needs Safari 17+, but PostsController#show
|
|
349
|
+
policy allows Safari 15
|
|
350
|
+
|
|
351
|
+
app/assets/builds/application.css
|
|
352
|
+
▲ 42:3 css-has ":has()" is not a known feature
|
|
353
|
+
|
|
354
|
+
Browser policies (2 found)
|
|
355
|
+
ApplicationController :modern
|
|
356
|
+
LegacyController { safari: 15, chrome: 100 } (only: embed)
|
|
357
|
+
|
|
358
|
+
1 error, 1 warning across 2 file(s)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
`[response] Controller#action` lines are findings against an endpoint's rendered HTML; ordinary file paths are findings against assets the endpoint loaded — the JSON output (`browsable replay … --format json`) preserves the full endpoint-to-finding mapping so dashboards can reconstruct it.
|
|
362
|
+
|
|
363
|
+
### Compatibility
|
|
364
|
+
|
|
365
|
+
- Rails 7.1+ (middleware reads `env["action_controller.instance"]`)
|
|
366
|
+
- Ruby 3.2+
|
|
367
|
+
- Propshaft (preferred), with a Sprockets + filesystem fallback
|
|
368
|
+
- RSpec 3.10+ or Minitest 5.15+
|
|
369
|
+
|
|
370
|
+
## Per-controller and per-action policies
|
|
371
|
+
|
|
372
|
+
Rails lets any controller override `allow_browser` and scope the override to certain actions with `only:` / `except:`. Browsable scans every file under `app/controllers/` (including `concerns/`) and lists each `allow_browser` call it finds — with its versions and any action scope — under **Browser policies** in the report.
|
|
373
|
+
|
|
374
|
+
In **static mode**, the audit runs against a single target. CSS and importmap JavaScript are global assets, included via layout helpers on nearly every page, so they have no single owning controller action — and a static asset → endpoint graph would be guesswork.
|
|
375
|
+
|
|
376
|
+
In **runtime mode** (v0.2+), this is solved properly: the middleware sees the actual HTML each endpoint renders during a test run, so findings are attached to the endpoints that *actually loaded* the asset, against each endpoint's *specific* policy. See [Runtime auditing](#runtime-auditing-test-suite-mode) above.
|
|
213
377
|
|
|
214
378
|
## Rake tasks
|
|
215
379
|
|
|
216
|
-
Inside a Rails app, the railtie registers:
|
|
380
|
+
Inside a Rails app, the railtie registers three tasks:
|
|
217
381
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
382
|
+
| Task | Behavior |
|
|
383
|
+
| --- | --- |
|
|
384
|
+
| `rake browsable:audit` | Audit `app/assets/builds/` as it stands |
|
|
385
|
+
| `rake browsable:audit:fresh` | Run `assets:precompile` first, then audit |
|
|
386
|
+
| `rake browsable:doctor` | Run the dependency check |
|
|
221
387
|
|
|
222
|
-
|
|
223
|
-
|
|
388
|
+
> ⚠️ **Browsable never precompiles assets on its own.** In CI, compose the pipeline explicitly:
|
|
389
|
+
> ```bash
|
|
390
|
+
> bundle exec rails assets:precompile && bundle exec browsable audit
|
|
391
|
+
> ```
|
|
224
392
|
|
|
225
393
|
## Contributing
|
|
226
394
|
|
|
227
|
-
This gem lives in the `browsable/` subdirectory of the
|
|
228
|
-
[monorepo](https://github.com/romanhood/browsable). To work on it:
|
|
395
|
+
This gem lives in the `browsable/` subdirectory of the [monorepo][monorepo]. To work on it:
|
|
229
396
|
|
|
230
397
|
```bash
|
|
231
|
-
|
|
398
|
+
git clone https://github.com/romanhood/browsable
|
|
399
|
+
cd browsable/browsable
|
|
232
400
|
bundle install
|
|
233
401
|
bundle exec rspec
|
|
234
402
|
```
|
|
235
403
|
|
|
236
|
-
Refresh the bundled compat
|
|
404
|
+
Refresh the bundled MDN browser-compat-data snapshot:
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
ruby bin/update-bcd-snapshot
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Bug reports and pull requests welcome. The monorepo has a [CONTRIBUTING.md][contributing] with the broader workflow.
|
|
237
411
|
|
|
238
412
|
## License
|
|
239
413
|
|
|
240
|
-
MIT — see the
|
|
414
|
+
[MIT][license] — see the LICENSE file at the monorepo root.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
<div align="center">
|
|
419
|
+
|
|
420
|
+
Made with care for Rails developers who refuse to add a `package.json` to their app. 🛤️
|
|
421
|
+
|
|
422
|
+
[Monorepo][monorepo] · [LSP server][lsp] · [Neovim plugin][nvim] · [Report an issue][issues]
|
|
423
|
+
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
[monorepo]: https://github.com/romanhood/browsable
|
|
427
|
+
[lsp]: https://github.com/romanhood/browsable/tree/main/browsable-lsp
|
|
428
|
+
[nvim]: https://github.com/romanhood/browsable/tree/main/browsable.nvim
|
|
429
|
+
[roadmap]: https://github.com/romanhood/browsable/blob/main/ROADMAP.md
|
|
430
|
+
[contributing]: https://github.com/romanhood/browsable/blob/main/CONTRIBUTING.md
|
|
431
|
+
[license]: https://github.com/romanhood/browsable/blob/main/LICENSE
|
|
432
|
+
[issues]: https://github.com/romanhood/browsable/issues
|
|
433
|
+
[browserslist]: https://github.com/browserslist/browserslist
|