twitter-bootstrap-rails 5.5.0 → 5.5.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 +23 -0
- data/README.md +32 -0
- data/lib/generators/bootstrap/agent/agent_generator.rb +5 -0
- data/lib/generators/bootstrap/agent/templates/guide.md.erb +12 -0
- data/lib/generators/bootstrap/doctor/doctor_generator.rb +12 -2
- data/lib/twitter/bootstrap/rails/doctor.rb +35 -4
- data/lib/twitter/bootstrap/rails/v6_migration.rb +156 -0
- data/lib/twitter/bootstrap/rails/version.rb +1 -1
- data/spec/lib/generators/agent_generator_spec.rb +9 -0
- data/spec/lib/generators/doctor_generator_spec.rb +56 -0
- data/spec/lib/twitter_bootstrap_rails/v6_migration_spec.rb +98 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 875edfcbadfdb5b579da81a68c3a5dc44d7b14d208bcf7de6dc89a8dabe969d3
|
|
4
|
+
data.tar.gz: bc3f64fe1f6c45217d043887a9fbcab629e7161738b2c47c3ea46a0a830b1cb1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18bd87131c776f3da869462fb6af504614dd708a91434881a9e46955a3e7817cdbcd8071e3d490a5bd18841b3601016e632e73163071786c026b5a2fb52cfc74
|
|
7
|
+
data.tar.gz: 75630ca82664a7956d088fbc90b48d2eb1f11e89087488dcb7a5b209221aff58ccabc2296546a4be9fc83595109b6b31fc41db9be8e931a23818ac8f6dea8841
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## 5.5.1
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Bootstrap 6 migration support.** `rails g bootstrap:doctor --v6` adds a
|
|
6
|
+
`v6_migration` check and list: Bootstrap 5 classes, data attributes, Sass
|
|
7
|
+
`@import`s and JavaScript calls that change in Bootstrap 6, with file, line and
|
|
8
|
+
replacement. It follows the upstream `bootstrap-v5-v6-migration` skill from
|
|
9
|
+
`twbs/bootstrap`, scans views, helpers, components, `app/javascript` and
|
|
10
|
+
stylesheets, skips the Bootstrap files this gem vendored, and only warns.
|
|
11
|
+
- The `bootstrap:agent` guide has a Bootstrap 6 section. It tells agents not to write
|
|
12
|
+
Bootstrap 6 markup on the Bootstrap 5 this gem ships, and gives the steps to migrate:
|
|
13
|
+
install the upstream skill with
|
|
14
|
+
`npx skills add twbs/bootstrap --skill bootstrap-v5-v6-migration`, run
|
|
15
|
+
`bootstrap:doctor --v6 --json`, and switch assets before markup.
|
|
16
|
+
- The upstream migration skill is installed in this repository
|
|
17
|
+
(`.claude/skills/bootstrap-v5-v6-migration`, pinned in `skills-lock.json`) for work
|
|
18
|
+
on the gem itself. It is not packaged into the gem.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `bootstrap:doctor` prints a heading above each list of findings.
|
|
23
|
+
|
|
1
24
|
## 5.5.0
|
|
2
25
|
|
|
3
26
|
### Added
|
data/README.md
CHANGED
|
@@ -156,6 +156,7 @@ knows instead. The gem doesn't call any AI service and needs no API key.
|
|
|
156
156
|
rails g bootstrap:agent # write a guide tailored to this app
|
|
157
157
|
rails g bootstrap:doctor # check how Bootstrap is set up
|
|
158
158
|
rails g bootstrap:doctor --json # the same report, for agents and CI
|
|
159
|
+
rails g bootstrap:doctor --v6 # also list what changes in Bootstrap 6
|
|
159
160
|
rails g bootstrap:header --json # list a snippet's variants as JSON
|
|
160
161
|
```
|
|
161
162
|
|
|
@@ -179,6 +180,37 @@ In an existing app, `legacy_markup` works as a migration to-do list. Plain names
|
|
|
179
180
|
If Bootstrap comes from `package.json` or your importmap, the doctor warns that it
|
|
180
181
|
can't verify it rather than reporting an error.
|
|
181
182
|
|
|
183
|
+
### Preparing for Bootstrap 6
|
|
184
|
+
|
|
185
|
+
This gem ships Bootstrap 5.3.8. Bootstrap 6 is in alpha (`6.0.0-alpha.1` on npm),
|
|
186
|
+
and the Bootstrap team publishes an agent skill for the move from 5 to 6. Install it
|
|
187
|
+
into your app alongside the gem's own guide:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
npx skills add twbs/bootstrap --skill bootstrap-v5-v6-migration
|
|
191
|
+
rails g bootstrap:agent # the guide tells agents to stay on Bootstrap 5 markup until you migrate
|
|
192
|
+
rails g bootstrap:doctor --v6 # the migration worklist
|
|
193
|
+
rails g bootstrap:doctor --v6 --json
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`--v6` adds a `v6_migration` check and list: every Bootstrap 5 class, data attribute,
|
|
197
|
+
Sass `@import` and JavaScript call that changes in Bootstrap 6, with file, line and
|
|
198
|
+
replacement. For example `modal-body` becomes `dialog-body`, `col-md-6` becomes
|
|
199
|
+
`md:col-6`, `btn-primary` becomes `btn-solid theme-primary`, and
|
|
200
|
+
`data-bs-toggle="dropdown"` becomes `data-bs-toggle="menu"`. It scans `app/views`,
|
|
201
|
+
`app/helpers`, `app/components`, `app/javascript` and your stylesheets, and skips the
|
|
202
|
+
Bootstrap files this gem vendored. It reports a warning, never an error, and is off
|
|
203
|
+
unless you ask for it.
|
|
204
|
+
|
|
205
|
+
Things the scan can't do for you:
|
|
206
|
+
|
|
207
|
+
- The vendored assets, `bootstrap:install` and every generator are still Bootstrap 5.
|
|
208
|
+
Switch the assets to Bootstrap 6 before changing markup, and don't re-run the
|
|
209
|
+
generators afterwards.
|
|
210
|
+
- The view helpers (`nav_bar`, `modal_dialog`, `badge` and the rest) render Bootstrap 5
|
|
211
|
+
markup at runtime, so their output isn't scanned.
|
|
212
|
+
- Bootstrap 6 changes the spacing scale, so check `p-*`/`m-*` utilities by eye.
|
|
213
|
+
|
|
182
214
|
## Quick Start
|
|
183
215
|
|
|
184
216
|
```bash
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'erb'
|
|
2
2
|
require 'rails/generators'
|
|
3
3
|
require 'twitter/bootstrap/rails/doctor'
|
|
4
|
+
require 'twitter/bootstrap/rails/v6_migration'
|
|
4
5
|
require 'generators/bootstrap/snippets/snippet_catalog'
|
|
5
6
|
|
|
6
7
|
module Bootstrap
|
|
@@ -66,6 +67,10 @@ module Bootstrap
|
|
|
66
67
|
Twitter::Bootstrap::Rails::LegacyMarkup
|
|
67
68
|
end
|
|
68
69
|
|
|
70
|
+
def v6
|
|
71
|
+
Twitter::Bootstrap::Rails::V6Migration
|
|
72
|
+
end
|
|
73
|
+
|
|
69
74
|
def pipeline_note
|
|
70
75
|
case facts[:asset_pipeline]
|
|
71
76
|
when "propshaft"
|
|
@@ -45,6 +45,18 @@ Snippets are always written as ERB partials. They render from any template engin
|
|
|
45
45
|
- Modals: `modal_dialog`, `modal_toggle`, `modal_cancel_button`
|
|
46
46
|
- Other: `badge`, `tag_label`, `render_breadcrumbs`, `add_breadcrumb`, and `error_span` on form builders
|
|
47
47
|
|
|
48
|
+
## Bootstrap 6
|
|
49
|
+
|
|
50
|
+
This app runs Bootstrap <%= facts[:bootstrap_version] %>. Don't write Bootstrap 6 markup (`dialog`, `drawer`, `menu`, `md:col-6`, `btn-solid theme-primary`) unless the app is being moved to Bootstrap 6: none of it works on <%= facts[:bootstrap_version] %>. The gem's generators and helpers produce Bootstrap 5 markup.
|
|
51
|
+
|
|
52
|
+
To move the app to Bootstrap 6:
|
|
53
|
+
|
|
54
|
+
1. Install the upstream migration skill: `<%= v6::SKILL_COMMAND %>`. Follow it alongside this guide.
|
|
55
|
+
2. Run `rails g bootstrap:doctor --v6 --json`. It lists every Bootstrap 5 class, data attribute, Sass import and JavaScript call that changes, with file, line and replacement, under `v6_migration`.
|
|
56
|
+
3. Switch the Bootstrap assets before changing markup, because the vendored files and `bootstrap:install` are Bootstrap 5. Re-running `bootstrap:layout`, `bootstrap:themed` or a snippet generator writes Bootstrap 5 markup again.
|
|
57
|
+
4. The helpers in "View helpers" render Bootstrap 5 markup at runtime, so the scan can't see their output. Replace them with Bootstrap 6 markup in the views that use them.
|
|
58
|
+
5. v6 changes the spacing scale (`p-3` is smaller), so check spacing utilities by eye; the scan doesn't flag them.
|
|
59
|
+
|
|
48
60
|
## Markup to avoid
|
|
49
61
|
|
|
50
62
|
These come from older Bootstrap versions and do nothing on this one. `bootstrap:doctor` reports them, with file and line, under `legacy_markup`. Plain names like `close` or `well` may be the app's own CSS classes, so check those before changing them.
|
|
@@ -11,9 +11,11 @@ module Bootstrap
|
|
|
11
11
|
|
|
12
12
|
class_option :json, :type => :boolean, :default => false,
|
|
13
13
|
:desc => 'Print the report as JSON'
|
|
14
|
+
class_option :v6, :type => :boolean, :default => false,
|
|
15
|
+
:desc => 'Also list Bootstrap 5 code that changes in Bootstrap 6'
|
|
14
16
|
|
|
15
17
|
def diagnose
|
|
16
|
-
report = Twitter::Bootstrap::Rails::Doctor.new(destination_root).report
|
|
18
|
+
report = Twitter::Bootstrap::Rails::Doctor.new(destination_root).report(:v6 => options[:v6])
|
|
17
19
|
|
|
18
20
|
options[:json] ? say(JSON.pretty_generate(report)) : print_report(report)
|
|
19
21
|
exit(1) if report[:status] == "error"
|
|
@@ -30,7 +32,15 @@ module Bootstrap
|
|
|
30
32
|
say " #{' ' * 20} Fix: #{check[:fix]}" if check[:fix]
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
report[:legacy_markup]
|
|
35
|
+
print_findings("Older Bootstrap markup", report[:legacy_markup])
|
|
36
|
+
print_findings("Changes in Bootstrap 6", report[:v6_migration])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def print_findings(title, findings)
|
|
40
|
+
return if findings.nil? || findings.empty?
|
|
41
|
+
|
|
42
|
+
say "\n#{title}:"
|
|
43
|
+
findings.each do |finding|
|
|
34
44
|
say " #{finding[:file]}:#{finding[:line]} #{finding[:found]} -> #{finding[:replacement]}"
|
|
35
45
|
end
|
|
36
46
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'twitter/bootstrap/rails/version'
|
|
2
2
|
require 'twitter/bootstrap/rails/config'
|
|
3
3
|
require 'twitter/bootstrap/rails/legacy_markup'
|
|
4
|
+
require 'twitter/bootstrap/rails/v6_migration'
|
|
4
5
|
|
|
5
6
|
module Twitter
|
|
6
7
|
module Bootstrap
|
|
@@ -14,19 +15,27 @@ module Twitter
|
|
|
14
15
|
SCANNED_EXTENSIONS = %w[.erb .haml .slim .rb].freeze
|
|
15
16
|
PLACEHOLDER_IMAGES = ["bootstrap-docs.png", "bootstrap-themes.png", "/docs/5.3/assets/", "unsplash-photo-"].freeze
|
|
16
17
|
SEVERITY = %w[ok warn error].freeze
|
|
18
|
+
# What `--v6` scans on top of views and helpers. The gem's own vendored
|
|
19
|
+
# Bootstrap files are skipped: replacing them is the first migration step.
|
|
20
|
+
V6_EXTRA_FILES = "app/{javascript,assets/javascripts,assets/stylesheets}/**/*".freeze
|
|
21
|
+
V6_EXTENSIONS = %w[.js .mjs .ts .css .scss .sass].freeze
|
|
22
|
+
VENDORED_DIR = %r{/twitter/bootstrap/}
|
|
17
23
|
|
|
18
24
|
def initialize(root)
|
|
19
25
|
@root = root
|
|
20
26
|
end
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
# v6: true adds a v6_migration check and list for apps moving to Bootstrap 6.
|
|
29
|
+
def report(v6: false)
|
|
23
30
|
checks = [css_check, js_check, version_check, sprite_check, placeholder_check, legacy_check]
|
|
31
|
+
checks << v6_check if v6
|
|
24
32
|
|
|
25
|
-
facts.merge(
|
|
33
|
+
result = facts.merge(
|
|
26
34
|
:status => checks.map { |c| c[:status] }.max_by { |s| SEVERITY.index(s) },
|
|
27
35
|
:checks => checks,
|
|
28
36
|
:legacy_markup => legacy_markup
|
|
29
37
|
)
|
|
38
|
+
v6 ? result.merge(:v6_migration => v6_migration) : result
|
|
30
39
|
end
|
|
31
40
|
|
|
32
41
|
def facts
|
|
@@ -169,6 +178,28 @@ module Twitter
|
|
|
169
178
|
end
|
|
170
179
|
end
|
|
171
180
|
|
|
181
|
+
def v6_check
|
|
182
|
+
count = v6_migration.size
|
|
183
|
+
return check("v6_migration", "ok", "Nothing found that changes in Bootstrap 6.") if count.zero?
|
|
184
|
+
|
|
185
|
+
check("v6_migration", "warn",
|
|
186
|
+
"#{count} use(s) of Bootstrap 5 markup, Sass or JavaScript that change in Bootstrap 6, listed in v6_migration. " \
|
|
187
|
+
"This gem still ships Bootstrap #{BOOTSTRAP_VERSION}, so only change them as part of moving the app to Bootstrap 6.",
|
|
188
|
+
"Install the upstream migration skill (`#{V6Migration::SKILL_COMMAND}`) and follow it, " \
|
|
189
|
+
"or read #{V6Migration::GUIDE_URL}. Switch the Bootstrap assets first.")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def v6_migration
|
|
193
|
+
@v6_migration ||= v6_files.flat_map do |file|
|
|
194
|
+
V6Migration.scan(read(file), File.extname(file)).map { |finding| { :file => file }.merge(finding) }
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def v6_files
|
|
199
|
+
extra = relative_files(V6_EXTRA_FILES, V6_EXTENSIONS).reject { |file| file.match?(VENDORED_DIR) }
|
|
200
|
+
(scanned_files + extra).uniq.sort
|
|
201
|
+
end
|
|
202
|
+
|
|
172
203
|
def check(id, status, message, fix = nil)
|
|
173
204
|
result = { :id => id, :status => status, :message => message }
|
|
174
205
|
result[:fix] = fix if fix
|
|
@@ -187,9 +218,9 @@ module Twitter
|
|
|
187
218
|
relative_files("app/{views,helpers,components}/**/*")
|
|
188
219
|
end
|
|
189
220
|
|
|
190
|
-
def relative_files(pattern)
|
|
221
|
+
def relative_files(pattern, extensions = SCANNED_EXTENSIONS)
|
|
191
222
|
Dir[File.join(@root, pattern)]
|
|
192
|
-
.select { |file| File.file?(file) &&
|
|
223
|
+
.select { |file| File.file?(file) && extensions.include?(File.extname(file)) }
|
|
193
224
|
.map { |file| file.delete_prefix("#{@root}/") }
|
|
194
225
|
.sort
|
|
195
226
|
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
require 'twitter/bootstrap/rails/legacy_markup'
|
|
2
|
+
|
|
3
|
+
module Twitter
|
|
4
|
+
module Bootstrap
|
|
5
|
+
module Rails
|
|
6
|
+
# Finds Bootstrap 5 markup, Sass and JavaScript that changes in Bootstrap 6,
|
|
7
|
+
# following the upstream migration skill
|
|
8
|
+
# (npx skills add twbs/bootstrap --skill bootstrap-v5-v6-migration).
|
|
9
|
+
# The gem still ships Bootstrap 5, so this is a worklist for an app that is
|
|
10
|
+
# moving to 6, not markup to avoid today. `bootstrap:doctor --v6` reports it.
|
|
11
|
+
module V6Migration
|
|
12
|
+
SKILL_COMMAND = "npx skills add twbs/bootstrap --skill bootstrap-v5-v6-migration".freeze
|
|
13
|
+
GUIDE_URL = "https://getbootstrap.com/docs/6.0/guides/migration/".freeze
|
|
14
|
+
|
|
15
|
+
BREAKPOINTS = { "sm" => "sm", "md" => "md", "lg" => "lg", "xl" => "xl", "xxl" => "2xl" }.freeze
|
|
16
|
+
THEMES = %w[primary secondary success danger warning info].freeze
|
|
17
|
+
FONT_SIZES = { "1" => "4xl", "2" => "3xl", "3" => "2xl", "4" => "xl", "5" => "lg", "6" => "md" }.freeze
|
|
18
|
+
RENAMED_COMPONENTS = { "modal" => "dialog", "offcanvas" => "drawer", "dropdown" => "menu" }.freeze
|
|
19
|
+
# Classes that take a breakpoint as a suffix in v5 (navbar-expand-lg).
|
|
20
|
+
SUFFIXED = %w[container navbar-expand offcanvas table-responsive list-group-horizontal vstack hstack col].freeze
|
|
21
|
+
|
|
22
|
+
CLASSES = {
|
|
23
|
+
"modal" => "dialog on a <dialog> element",
|
|
24
|
+
"modal-dialog" => "nothing (remove the wrapper; use one <dialog class=\"dialog\">)",
|
|
25
|
+
"modal-content" => "nothing (remove the wrapper; use one <dialog class=\"dialog\">)",
|
|
26
|
+
"offcanvas" => "drawer on a <dialog> element",
|
|
27
|
+
"dropdown" => "nothing (remove the wrapper; the toggle and .menu are siblings)",
|
|
28
|
+
"dropdown-menu" => "menu",
|
|
29
|
+
"dropdown-item" => "menu-item",
|
|
30
|
+
"dropdown-divider" => "menu-divider",
|
|
31
|
+
"dropdown-header" => "menu-header",
|
|
32
|
+
"dropdown-toggle" => "nothing (the menu toggle needs no class)",
|
|
33
|
+
"dropdown-toggle-split" => "nothing (remove it)",
|
|
34
|
+
"text-muted" => "fg-secondary",
|
|
35
|
+
"form-select" => "form-control (on the select)",
|
|
36
|
+
"form-check" => "form-field",
|
|
37
|
+
"form-check-input" => "check or radio (on the input)",
|
|
38
|
+
"form-check-label" => "nothing (a plain label)",
|
|
39
|
+
"form-switch" => "a switch wrapper around the input",
|
|
40
|
+
"needs-validation" => "data-bs-validate on the form",
|
|
41
|
+
"was-validated" => "nothing (:user-invalid handles it)",
|
|
42
|
+
"has-validation" => "nothing (remove it)",
|
|
43
|
+
"clearfix" => "d-flow-root",
|
|
44
|
+
"lh-base" => "lh-md",
|
|
45
|
+
"lead" => "fs-* and fw-* utilities",
|
|
46
|
+
"placeholder-glow" => "placeholder-pulse (or placeholder-wave)",
|
|
47
|
+
"btn-close-white" => "nothing (set the text colour instead)",
|
|
48
|
+
"card-link" => "a plain link",
|
|
49
|
+
"accordion-button" => "nothing (put the title in summary.accordion-header)",
|
|
50
|
+
"accordion-collapse" => "nothing (use details.accordion-item)",
|
|
51
|
+
"navbar-light" => "nothing (remove it)",
|
|
52
|
+
"navbar-dark" => "nothing (use data-bs-theme=\"dark\")",
|
|
53
|
+
"carousel-control-prev" => "btn-icon with data-bs-slide=\"prev\"",
|
|
54
|
+
"carousel-control-next" => "btn-icon with data-bs-slide=\"next\"",
|
|
55
|
+
"carousel-control-prev-icon" => "carousel-icon-prev",
|
|
56
|
+
"carousel-control-next-icon" => "carousel-icon-next",
|
|
57
|
+
"carousel-caption" => "your own markup",
|
|
58
|
+
"carousel-dark" => "data-bs-theme=\"dark\""
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
# [pattern, block turning the match into its replacement, example for docs]
|
|
62
|
+
CLASS_PATTERNS = [
|
|
63
|
+
[/\Amodal-(header|body|footer|title|sm|lg|xl|fullscreen)\z/, ->(m) { "dialog-#{m[1]}" }, "modal-body becomes dialog-body"],
|
|
64
|
+
[/\Aoffcanvas-(header|body|title|start|end|top|bottom)\z/, ->(m) { "drawer-#{m[1]}" }, "offcanvas-start becomes drawer-start"],
|
|
65
|
+
[/\Adropdown-menu-(start|end)\z/, ->(m) { "menu-#{m[1]}" }, "dropdown-menu-end becomes menu-end"],
|
|
66
|
+
[/\Amodal-fullscreen-(sm|md|lg|xl|xxl)-down\z/,
|
|
67
|
+
->(m) { "#{BREAKPOINTS[m[1]]}-down:dialog-fullscreen" }, "modal-fullscreen-sm-down becomes sm-down:dialog-fullscreen"],
|
|
68
|
+
[/\A([a-z][\w-]*)-hover\z/,
|
|
69
|
+
->(m) { "hover:#{V6Migration.class_replacement(m[1])}" }, "opacity-50-hover becomes hover:opacity-50"],
|
|
70
|
+
[/\Abtn-(outline-)?(#{THEMES.join('|')})\z/,
|
|
71
|
+
->(m) { "btn-#{m[1] ? 'outline' : 'solid'} theme-#{m[2]}" }, "btn-primary and btn-outline-primary become btn-solid theme-primary and btn-outline theme-primary"],
|
|
72
|
+
[/\Aalert-(#{THEMES.join('|')})\z/, ->(m) { "alert theme-#{m[1]}" }, "alert-info becomes alert theme-info"],
|
|
73
|
+
[/\Atext-bg-(#{THEMES.join('|')})\z/, ->(m) { "bg-#{m[1]} fg-contrast-#{m[1]}" }, "text-bg-warning becomes bg-warning fg-contrast-warning"],
|
|
74
|
+
[/\Atext-(#{THEMES.join('|')})\z/, ->(m) { "fg-#{m[1]}" }, "text-danger becomes fg-danger"],
|
|
75
|
+
[/\A(?:btn|btn-outline|alert|text|text-bg)-(light|dark)\z/,
|
|
76
|
+
->(_) { "a neutral surface, a colour mode or theme-inverse (v6 has no theme-light or theme-dark)" },
|
|
77
|
+
"btn-light and text-dark become a neutral surface, a colour mode or theme-inverse"],
|
|
78
|
+
[/\Afs-([1-6])\z/, ->(m) { "fs-#{FONT_SIZES[m[1]]}" }, "fs-1 and fs-6 become fs-4xl and fs-md"],
|
|
79
|
+
[/\Adisplay-[1-6]\z/, ->(_) { "fs-* and fw-* utilities" }, "display-1 becomes fs-* and fw-* utilities"],
|
|
80
|
+
[/\Am(h|w)-(.+)\z/, ->(m) { "max-#{m[1]}-#{m[2]}" }, "mw-100 becomes max-w-100"],
|
|
81
|
+
[/\Alink-(offset|underline)(-.+)?\z/,
|
|
82
|
+
->(m) { "underline#{m[1] == 'offset' ? '-offset' : ''}#{m[2]}" }, "link-offset-2 becomes underline-offset-2"],
|
|
83
|
+
[/\Am[tbsexy]?-n\d\z/, ->(_) { "ms--1, ms--2, me--1, me--2 or your own style" }, "mt-n1 becomes ms--1 / me--1 or your own style"],
|
|
84
|
+
[/\A(#{SUFFIXED.join('|')})-(sm|md|lg|xl|xxl)\z/,
|
|
85
|
+
->(m) { "#{BREAKPOINTS[m[2]]}:#{V6Migration.class_replacement(m[1])}" }, "navbar-expand-lg becomes lg:navbar-expand"],
|
|
86
|
+
[/\A([a-z][\w-]*?)-print-([\w-]+)\z/,
|
|
87
|
+
->(m) { "print:#{V6Migration.class_replacement("#{m[1]}-#{m[2]}")}" }, "d-print-none becomes print:d-none"],
|
|
88
|
+
[/\A([a-z][\w-]*?)-(sm|md|lg|xl|xxl)-([\w-]+)\z/,
|
|
89
|
+
->(m) { "#{BREAKPOINTS[m[2]]}:#{V6Migration.class_replacement("#{m[1]}-#{m[3]}")}" },
|
|
90
|
+
"col-md-6 and d-xxl-none become md:col-6 and 2xl:d-none"]
|
|
91
|
+
].freeze
|
|
92
|
+
|
|
93
|
+
TOGGLE = /(?:data-bs-|\bbs[_-])(toggle|dismiss)["']?\s*(?:=>|=|:)\s*(["'])(#{RENAMED_COMPONENTS.keys.join('|')})\2/
|
|
94
|
+
|
|
95
|
+
# [pattern, replacement, example for docs]. Applied to every scanned file.
|
|
96
|
+
LINE_PATTERNS = [
|
|
97
|
+
[/(?:data-bs-|\bbs[_-])ride(?![\w-])/, %{data-bs-autoplay="true" (carousels no longer start on their own)}],
|
|
98
|
+
[/@import\s+(["'])bootstrap/, %{@use "bootstrap/scss/bootstrap" (add `as *` to keep v5 names)}],
|
|
99
|
+
[/\bbootstrap\.(?:Modal|Offcanvas|Dropdown)\b/, "import { Dialog, Drawer, Menu } from Bootstrap's module build"],
|
|
100
|
+
[/\bpopperConfig\b/, "floatingConfig"],
|
|
101
|
+
[/@popperjs\/core/, "@floating-ui/dom"],
|
|
102
|
+
[/--bs-[\w-]+-rgb\b/, "color-mix(in oklab, var(--bs-…), transparent N%)"]
|
|
103
|
+
].freeze
|
|
104
|
+
|
|
105
|
+
# Files whose class attributes and shorthand are read for class names.
|
|
106
|
+
MARKUP_EXTENSIONS = %w[.erb .haml .slim .rb .html].freeze
|
|
107
|
+
|
|
108
|
+
module_function
|
|
109
|
+
|
|
110
|
+
# Returns [{ :line, :found, :replacement }, ...] for a file's contents.
|
|
111
|
+
def scan(text, extension)
|
|
112
|
+
text.each_line.with_index(1).flat_map do |line, number|
|
|
113
|
+
scan_line(line, extension).map { |found, replacement| { :line => number, :found => found, :replacement => replacement } }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def scan_line(line, extension)
|
|
118
|
+
classes = MARKUP_EXTENSIONS.include?(extension) ? LegacyMarkup.class_names(line, extension) : []
|
|
119
|
+
renamed = classes.filter_map do |name|
|
|
120
|
+
replacement = replacement_for(name)
|
|
121
|
+
[name, replacement] if replacement
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
toggles = line.scan(TOGGLE).map do |attribute, _quote, value|
|
|
125
|
+
[%{data-bs-#{attribute}="#{value}"}, %{data-bs-#{attribute}="#{RENAMED_COMPONENTS[value]}"}]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
other = LINE_PATTERNS.flat_map do |pattern, replacement|
|
|
129
|
+
matches = []
|
|
130
|
+
line.scan(pattern) { matches << [Regexp.last_match(0).sub(/\Abs[_-]/, "data-bs-"), replacement] }
|
|
131
|
+
matches
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
renamed + toggles + other
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def replacement_for(name)
|
|
138
|
+
return CLASSES[name] if CLASSES.key?(name)
|
|
139
|
+
|
|
140
|
+
CLASS_PATTERNS.each do |pattern, replacement, _example|
|
|
141
|
+
match = pattern.match(name)
|
|
142
|
+
return replacement.call(match) if match
|
|
143
|
+
end
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The v6 name for a class when it is a plain rename, otherwise the class itself.
|
|
148
|
+
# Used under a breakpoint prefix, where a sentence would make no sense.
|
|
149
|
+
def class_replacement(name)
|
|
150
|
+
replacement = RENAMED_COMPONENTS[name] || replacement_for(name)
|
|
151
|
+
replacement && replacement.match?(/\A[\w:-]+\z/) ? replacement : name
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -107,5 +107,14 @@ describe Bootstrap::Generators::AgentGenerator do
|
|
|
107
107
|
|
|
108
108
|
expect(skill).to include("rails g bootstrap:doctor --json")
|
|
109
109
|
end
|
|
110
|
+
|
|
111
|
+
it "keeps agents on the shipped Bootstrap and points them at the upstream skill for Bootstrap 6" do
|
|
112
|
+
run_generator(described_class)
|
|
113
|
+
section = skill[/^## Bootstrap 6\n.*?(?=^## |\z)/m]
|
|
114
|
+
|
|
115
|
+
expect(section).to include("Don't write Bootstrap 6 markup")
|
|
116
|
+
expect(section).to include(Twitter::Bootstrap::Rails::V6Migration::SKILL_COMMAND)
|
|
117
|
+
expect(section).to include("rails g bootstrap:doctor --v6 --json")
|
|
118
|
+
end
|
|
110
119
|
end
|
|
111
120
|
end
|
|
@@ -166,6 +166,55 @@ describe Bootstrap::Generators::DoctorGenerator do
|
|
|
166
166
|
end
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
+
describe "Bootstrap 6 migration" do
|
|
170
|
+
def v6_report
|
|
171
|
+
rails_mod::Doctor.new(destination_root).report(:v6 => true)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "is left out unless asked for" do
|
|
175
|
+
write_destination("app/views/tasks/index.html.erb", %{<div class="modal-body"></div>})
|
|
176
|
+
|
|
177
|
+
expect(report).not_to have_key(:v6_migration)
|
|
178
|
+
expect(report[:checks].map { |c| c[:id] }).not_to include("v6_migration")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it "lists what changes in Bootstrap 6, with the location" do
|
|
182
|
+
write_destination("app/views/tasks/index.html.erb", %{<p>ok</p>\n<button data-bs-toggle="modal" class="btn btn-primary">x</button>})
|
|
183
|
+
|
|
184
|
+
expect(v6_report[:v6_migration]).to contain_exactly(
|
|
185
|
+
a_hash_including(:file => "app/views/tasks/index.html.erb", :line => 2, :found => "btn-primary"),
|
|
186
|
+
a_hash_including(:file => "app/views/tasks/index.html.erb", :line => 2, :found => %{data-bs-toggle="modal"})
|
|
187
|
+
)
|
|
188
|
+
check = v6_report[:checks].find { |c| c[:id] == "v6_migration" }
|
|
189
|
+
expect(check).to include(:status => "warn", :fix => a_string_including("bootstrap-v5-v6-migration"))
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "scans the app's JavaScript and stylesheets" do
|
|
193
|
+
write_destination("app/javascript/controllers/menu_controller.js", "new bootstrap.Dropdown(this.element)")
|
|
194
|
+
write_destination("app/assets/stylesheets/custom.scss", %{@import "bootstrap/scss/functions";})
|
|
195
|
+
|
|
196
|
+
expect(v6_report[:v6_migration].map { |f| f[:file] })
|
|
197
|
+
.to eq(%w[app/assets/stylesheets/custom.scss app/javascript/controllers/menu_controller.js])
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "skips the Bootstrap files the gem vendored" do
|
|
201
|
+
install_static_on_propshaft
|
|
202
|
+
layout_findings = v6_report[:v6_migration].reject { |f| f[:file].start_with?("app/views/") }
|
|
203
|
+
|
|
204
|
+
expect(layout_findings).to be_empty
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it "finds the Bootstrap 5 markup in a layout the generators wrote" do
|
|
208
|
+
install_static_on_propshaft
|
|
209
|
+
|
|
210
|
+
expect(v6_report[:v6_migration].map { |f| f[:found] }).to include("navbar-expand-lg")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it "is happy when nothing needs to change" do
|
|
214
|
+
expect(v6_report[:checks].find { |c| c[:id] == "v6_migration" }[:status]).to eq("ok")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
169
218
|
describe "facts" do
|
|
170
219
|
it "states the versions and template engine" do
|
|
171
220
|
expect(report).to include(
|
|
@@ -207,6 +256,13 @@ describe Bootstrap::Generators::DoctorGenerator do
|
|
|
207
256
|
.to raise_error(SystemExit) { |e| expect(e.status).to eq(1) }
|
|
208
257
|
end
|
|
209
258
|
|
|
259
|
+
it "adds the Bootstrap 6 worklist with --v6" do
|
|
260
|
+
parsed = JSON.parse(run_generator(described_class, %w[--v6 --json]))
|
|
261
|
+
|
|
262
|
+
expect(parsed["v6_migration"]).to include(a_hash_including("found" => "navbar-expand-lg", "replacement" => "lg:navbar-expand"))
|
|
263
|
+
expect(run_generator(described_class, %w[--v6])).to match(%r{\[warn\]\s+v6_migration.*\n(.*\n)*\s+app/views/layouts/application\.html\.erb:\d+\s+navbar-expand-lg -> lg:navbar-expand})
|
|
264
|
+
end
|
|
265
|
+
|
|
210
266
|
it "exits normally on warnings" do
|
|
211
267
|
write_destination("app/views/tasks/index.html.erb", %{<a class="btn-default"></a>})
|
|
212
268
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'twitter/bootstrap/rails/v6_migration'
|
|
3
|
+
|
|
4
|
+
describe Twitter::Bootstrap::Rails::V6Migration do
|
|
5
|
+
def scan(text, extension = ".erb")
|
|
6
|
+
described_class.scan(text, extension)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def found(text, extension = ".erb")
|
|
10
|
+
scan(text, extension).map { |f| f[:found] }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def replacements(text, extension = ".erb")
|
|
14
|
+
scan(text, extension).map { |f| f[:replacement] }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "reports the line and the replacement" do
|
|
18
|
+
finding = scan(%{<p>ok</p>\n<div class="modal-body">x</div>}).first
|
|
19
|
+
|
|
20
|
+
expect(finding).to include(:line => 2, :found => "modal-body", :replacement => "dialog-body")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "renames modal, offcanvas and dropdown classes" do
|
|
24
|
+
expect(replacements(%{<div class="offcanvas offcanvas-start dropdown-menu dropdown-item"></div>}))
|
|
25
|
+
.to eq(["drawer on a <dialog> element", "drawer-start", "menu", "menu-item"])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "renames components under a breakpoint prefix" do
|
|
29
|
+
expect(replacements(%{<div class="offcanvas-lg dropdown-menu-md-end"></div>})).to eq(%w[lg:drawer md:menu-end])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "moves breakpoints to a prefix" do
|
|
33
|
+
expect(replacements(%{<div class="col-md-6 d-lg-none p-sm-3 offset-xxl-2 sticky-md-top"></div>}))
|
|
34
|
+
.to eq(%w[md:col-6 lg:d-none sm:p-3 2xl:offset-2 md:sticky-top])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "moves breakpoint suffixes to a prefix" do
|
|
38
|
+
expect(replacements(%{<nav class="navbar navbar-expand-lg"><div class="container-xxl table-responsive-sm"></div></nav>}))
|
|
39
|
+
.to eq(%w[lg:navbar-expand 2xl:container sm:table-responsive])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "moves hover and print states to a prefix" do
|
|
43
|
+
expect(replacements(%{<a class="opacity-50-hover link-underline-opacity-75-hover d-print-none"></a>}))
|
|
44
|
+
.to eq(%w[hover:opacity-50 hover:underline-opacity-75 print:d-none])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "replaces colour variants with theme classes" do
|
|
48
|
+
expect(replacements(%{<a class="btn btn-primary btn-outline-danger alert-info text-muted text-success"></a>}))
|
|
49
|
+
.to eq(["btn-solid theme-primary", "btn-outline theme-danger", "alert theme-info", "fg-secondary", "fg-success"])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "maps font sizes to the t-shirt scale" do
|
|
53
|
+
expect(replacements(%{<p class="fs-1 fs-6"></p>})).to eq(%w[fs-4xl fs-md])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "finds renamed toggle values in HTML and Ruby" do
|
|
57
|
+
expect(replacements(%{<button data-bs-toggle="modal" data-bs-dismiss='offcanvas'></button>}))
|
|
58
|
+
.to eq([%{data-bs-toggle="dialog"}, %{data-bs-dismiss="drawer"}])
|
|
59
|
+
expect(replacements(%{<%= link_to "x", "#", data: { bs_toggle: "dropdown" } %>}))
|
|
60
|
+
.to eq([%{data-bs-toggle="menu"}])
|
|
61
|
+
expect(replacements(%{<%= button_tag "x", data: { "bs-dismiss" => "modal" } %>}))
|
|
62
|
+
.to eq([%{data-bs-dismiss="dialog"}])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "finds carousel autoplay" do
|
|
66
|
+
expect(found(%{<div class="carousel" data-bs-ride="carousel"></div>})).to eq(%w[data-bs-ride])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "finds Sass and JavaScript that changed" do
|
|
70
|
+
expect(found(%{@import "bootstrap/scss/bootstrap";}, ".scss")).to eq([%{@import "bootstrap}])
|
|
71
|
+
expect(found("new bootstrap.Modal(el, { popperConfig: {} })", ".js")).to eq(%w[bootstrap.Modal popperConfig])
|
|
72
|
+
expect(found("color: rgba(var(--bs-primary-rgb), .5);", ".css")).to eq(%w[--bs-primary-rgb])
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "only reads class names in markup and Ruby files" do
|
|
76
|
+
expect(found(%{const html = '<div class="modal-body">'}, ".js")).to be_empty
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "finds class shorthand in Haml and Slim" do
|
|
80
|
+
expect(found("%button.btn.btn-primary Save", ".haml")).to eq(%w[btn-primary])
|
|
81
|
+
expect(found(".modal-dialog\n .modal-content", ".slim")).to eq(%w[modal-dialog modal-content])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "leaves Bootstrap 6 markup alone" do
|
|
85
|
+
v6 = %{<dialog class="dialog"><div class="dialog-body md:col-6 btn-solid theme-primary fg-secondary fs-4xl"></div></dialog>}
|
|
86
|
+
|
|
87
|
+
expect(found(v6)).to be_empty
|
|
88
|
+
expect(found(%{<button data-bs-toggle="dialog"></button>})).to be_empty
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "matches whole class names only" do
|
|
92
|
+
expect(found(%{<div class="my-modal modal-bodywork"></div>})).to be_empty
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "has an example for every pattern, for the agent guide" do
|
|
96
|
+
expect(described_class::CLASS_PATTERNS.map(&:last)).to all(match(/ becomes? /))
|
|
97
|
+
end
|
|
98
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: twitter-bootstrap-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.5.
|
|
4
|
+
version: 5.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Seyhun Akyurek
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-11-
|
|
11
|
+
date: 2026-11-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|
|
@@ -196,6 +196,7 @@ files:
|
|
|
196
196
|
- lib/twitter/bootstrap/rails/doctor.rb
|
|
197
197
|
- lib/twitter/bootstrap/rails/engine.rb
|
|
198
198
|
- lib/twitter/bootstrap/rails/legacy_markup.rb
|
|
199
|
+
- lib/twitter/bootstrap/rails/v6_migration.rb
|
|
199
200
|
- lib/twitter/bootstrap/rails/version.rb
|
|
200
201
|
- spec/lib/breadcrumbs_spec.rb
|
|
201
202
|
- spec/lib/generators/agent_generator_spec.rb
|
|
@@ -215,6 +216,7 @@ files:
|
|
|
215
216
|
- spec/lib/twitter_bootstrap_rails/modal_helper_spec.rb
|
|
216
217
|
- spec/lib/twitter_bootstrap_rails/navbar_helper_spec.rb
|
|
217
218
|
- spec/lib/twitter_bootstrap_rails/uri_state_spec.rb
|
|
219
|
+
- spec/lib/twitter_bootstrap_rails/v6_migration_spec.rb
|
|
218
220
|
- spec/spec_helper.rb
|
|
219
221
|
- spec/support/generator_spec_helper.rb
|
|
220
222
|
- vendor/assets/javascripts/twitter/bootstrap/bootstrap.bundle.min.js
|
|
@@ -261,5 +263,6 @@ test_files:
|
|
|
261
263
|
- spec/lib/twitter_bootstrap_rails/modal_helper_spec.rb
|
|
262
264
|
- spec/lib/twitter_bootstrap_rails/navbar_helper_spec.rb
|
|
263
265
|
- spec/lib/twitter_bootstrap_rails/uri_state_spec.rb
|
|
266
|
+
- spec/lib/twitter_bootstrap_rails/v6_migration_spec.rb
|
|
264
267
|
- spec/spec_helper.rb
|
|
265
268
|
- spec/support/generator_spec_helper.rb
|