bootstrap-vendor 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: daa78a564f02b991f124b684302f7ff84ecca051e8948f6b7073d3b9f184f635
4
+ data.tar.gz: 48776d4c1ab31eedf4c05bcfee16ef540a783c4cf4d95391a7ff7e7b89c1d1b0
5
+ SHA512:
6
+ metadata.gz: 5a248e243d6597587e748e276d50e33c4a745ae25a5996ff5c0e74cf61d779b061aa8beb85255b92f804fc662a3155a2ba7c7768404c9a0c6400d0bde3925c2a
7
+ data.tar.gz: 34301886d1cb24a56b89d95b487a4c855b5bc933587944e964e3b28e30ffc15e11a0153f2b268432b25c7272a5a76ac6d7a58a8d1348a6307767e93da4c64be6
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.1.0] - 2026-03-26
6
+
7
+ - Add rake tasks: vendor, version, latest, status, init, install, update, uninstall
8
+ - Add Railtie to load rake tasks in Rails apps
9
+ - Add Registry with all stable Bootstrap versions and constraint matching via Gem::Requirement
10
+ - Add VersionFile to read/write .bootstrap-version files
11
+ - Add Config for ENV-based settings with opinionated defaults
12
+ - Add FileList to compute expected/installed files and download with source fallback
13
+ - Add download source fallback chain (JSDelivrNPM, JSDelivrGitHub, GitHubRaw, GitHubAPI)
14
+ - Support BOOTSTRAP_VENDOR_SOURCE ENV var to pin a specific download source
15
+ - Extract version list to flat VERSIONS.txt file, read by Registry at runtime
16
+ - Add `rake versions` maintainer task to update VERSIONS.txt from GitHub releases
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "bootstrap-vendor" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["veganstraightedge@gmail.com"](mailto:"veganstraightedge@gmail.com").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Shane Becker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,273 @@
1
+ # bootstrap-vendor
2
+
3
+ Rake tasks to vendor [Bootstrap](https://getbootstrap.com) CSS and JS into your Rails app. No asset pipeline, no npm, no build system. Just static files in `vendor/`.
4
+
5
+ ## Install
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem 'bootstrap-vendor'
11
+ ```
12
+
13
+ Then:
14
+
15
+ ```sh
16
+ bundle install
17
+ ```
18
+
19
+ In a Rails app, the rake tasks are loaded automatically via Railtie.
20
+
21
+ ## Quick start
22
+
23
+ ```sh
24
+ rake bootstrap:vendor
25
+ ```
26
+
27
+ This creates a `.bootstrap-version` file and downloads the latest Bootstrap CSS and JS into `vendor/stylesheets/` and `vendor/javascript/`.
28
+
29
+ ## Usage
30
+
31
+ ### In Rails
32
+
33
+ Add Bootstrap to your asset paths. In `config/initializers/assets.rb`:
34
+
35
+ ```ruby
36
+ Rails.application.config.assets.paths << Rails.root.join('vendor/stylesheets')
37
+ Rails.application.config.assets.paths << Rails.root.join('vendor/javascript')
38
+ ```
39
+
40
+ Or with importmaps, pin the JS in `config/importmap.rb`:
41
+
42
+ ```ruby
43
+ pin 'bootstrap', to: 'bootstrap.bundle.js'
44
+ ```
45
+
46
+ And add a stylesheet link in your layout:
47
+
48
+ ```erb
49
+ <%= stylesheet_link_tag 'bootstrap', 'data-turbo-track': 'reload' %>
50
+ ```
51
+
52
+ ### In non-Rails
53
+
54
+ Add the gem:
55
+
56
+ ```sh
57
+ gem install bootstrap-vendor
58
+ ```
59
+
60
+ Create a `Rakefile` (or add to an existing one):
61
+
62
+ ```ruby
63
+ require 'bootstrap/vendor'
64
+ load 'tasks/bootstrap.rake'
65
+ ```
66
+
67
+ Then use the rake tasks as normal:
68
+
69
+ ```sh
70
+ rake bootstrap:vendor
71
+ ```
72
+
73
+ ### From zsh (command line shell)
74
+
75
+ zsh interprets `[]` as glob patterns. Escape the brackets when passing arguments to rake tasks:
76
+
77
+ ```sh
78
+ rake 'bootstrap:init[overwrite]'
79
+ rake bootstrap:init\[overwrite\]
80
+ noglob rake bootstrap:init[overwrite]
81
+ ```
82
+
83
+ ## Rake tasks
84
+
85
+ - [`bootstrap:vendor`](#bootstrapvendor) — do it all shortcut
86
+ - [`bootstrap:version`](#bootstrapversion) — print current version
87
+ - [`bootstrap:latest`](#bootstraplatest) — print latest upstream version
88
+ - [`bootstrap:status`](#bootstrapstatus) — compare current to latest
89
+ - [`bootstrap:init`](#bootstrapinit) — create .bootstrap-version
90
+ - [`bootstrap:install`](#bootstrapinstall) — download files
91
+ - [`bootstrap:update`](#bootstrapupdate) — update files
92
+ - [`bootstrap:uninstall`](#bootstrapuninstall) — remove files
93
+
94
+ ### `bootstrap:vendor`
95
+
96
+ The opinionated, does-it-all shortcut. Creates `.bootstrap-version`, downloads the latest Bootstrap files, and you're done.
97
+
98
+ ```sh
99
+ rake bootstrap:vendor
100
+ ```
101
+
102
+ ### `bootstrap:version`
103
+
104
+ Prints the current version from `.bootstrap-version`.
105
+
106
+ ```sh
107
+ rake bootstrap:version
108
+ # 5.3.8
109
+ ```
110
+
111
+ ### `bootstrap:latest`
112
+
113
+ Prints the latest known Bootstrap version. Accepts an optional constraint.
114
+
115
+ ```sh
116
+ rake bootstrap:latest
117
+ # 5.3.8
118
+
119
+ rake bootstrap:latest[5]
120
+ # 5.3.8
121
+
122
+ rake bootstrap:latest[4]
123
+ # 4.6.2
124
+
125
+ rake bootstrap:latest[5.2]
126
+ # 5.2.3
127
+ ```
128
+
129
+ ### `bootstrap:status`
130
+
131
+ Compares your current version to the latest upstream.
132
+
133
+ ```sh
134
+ rake bootstrap:status
135
+ # Current version 5.3.8 is up to date
136
+
137
+ rake bootstrap:status
138
+ # Current version 5.2.3 is behind latest version 5.3.8
139
+ ```
140
+
141
+ ### `bootstrap:init`
142
+
143
+ Creates a `.bootstrap-version` file. Defaults to the latest version.
144
+
145
+ ```sh
146
+ rake bootstrap:init
147
+ # A .bootstrap-version file was created.
148
+ # Version: 5.3.8
149
+
150
+ rake bootstrap:init[4]
151
+ # A .bootstrap-version file was created.
152
+ # Version: 4.6.2
153
+
154
+ rake bootstrap:init[overwrite]
155
+ # Overwrites an existing .bootstrap-version
156
+ ```
157
+
158
+ ### `bootstrap:install`
159
+
160
+ Downloads Bootstrap CSS and JS files. Fails if already installed (use `update` instead).
161
+
162
+ ```sh
163
+ rake bootstrap:install
164
+ ```
165
+
166
+ ### `bootstrap:update`
167
+
168
+ Downloads Bootstrap CSS and JS files, overwriting existing ones.
169
+
170
+ ```sh
171
+ rake bootstrap:update
172
+ # Bootstrap updated from 5.3.5 to 5.3.8
173
+ ```
174
+
175
+ ### `bootstrap:uninstall`
176
+
177
+ Removes vendored Bootstrap files and `.bootstrap-version`.
178
+
179
+ ```sh
180
+ rake bootstrap:uninstall
181
+ ```
182
+
183
+ ## Default files
184
+
185
+ By default, these files are downloaded:
186
+
187
+ ```sh
188
+ vendor/stylesheets/bootstrap.css
189
+ vendor/stylesheets/bootstrap.css.map
190
+ vendor/javascript/bootstrap.bundle.js
191
+ vendor/javascript/bootstrap.bundle.js.map
192
+ ```
193
+
194
+ ### Configuration
195
+
196
+ Customize which files are downloaded with environment variables. These are the defaults:
197
+
198
+ | ENV variable | Default | Effect |
199
+ | ---------------------------- | ------- | ------------------------------------------ |
200
+ | `BOOTSTRAP_VENDOR_CSS_LTR` | `true` | Download LTR CSS |
201
+ | `BOOTSTRAP_VENDOR_CSS_RTL` | `false` | Download RTL CSS |
202
+ | `BOOTSTRAP_VENDOR_CSS_MAP` | `true` | Download CSS source maps |
203
+ | `BOOTSTRAP_VENDOR_CSS_MIN` | `false` | Download minified CSS |
204
+ | `BOOTSTRAP_VENDOR_JS_BUNDLE` | `true` | Download bundle (includes Popper) vs plain |
205
+ | `BOOTSTRAP_VENDOR_JS_MAP` | `true` | Download JS source maps |
206
+ | `BOOTSTRAP_VENDOR_JS_MIN` | `false` | Download minified JS |
207
+
208
+ At least one of `CSS_LTR` or `CSS_RTL` must be `true`.
209
+
210
+ Example: download minified files without source maps:
211
+
212
+ ```sh
213
+ BOOTSTRAP_VENDOR_CSS_MIN=true BOOTSTRAP_VENDOR_CSS_MAP=false BOOTSTRAP_VENDOR_JS_MIN=true BOOTSTRAP_VENDOR_JS_MAP=false rake bootstrap:install
214
+ ```
215
+
216
+ ## Download sources
217
+
218
+ Files are downloaded from multiple sources with automatic fallback for resilience:
219
+
220
+ 1. [jsdelivr CDN](https://www.jsdelivr.com) (npm)
221
+ 2. [jsdelivr CDN](https://www.jsdelivr.com) (GitHub)
222
+ 3. [GitHub raw files](https://raw.githubusercontent.com)
223
+ 4. [GitHub releases](https://github.com/twbs/bootstrap/releases) (zip)
224
+
225
+ Pin a specific source with:
226
+
227
+ ```sh
228
+ BOOTSTRAP_VENDOR_SOURCE=github_raw rake bootstrap:install
229
+ ```
230
+
231
+ Valid source names: `jsdelivr_npm`, `jsdelivr_github`, `github_raw`, `github_api`.
232
+
233
+ ## Optional arguments
234
+
235
+ ### Path, where to install
236
+
237
+ Most tasks accept an optional `path` argument. By default, tasks operate in the current directory. Pass a path to target a different location:
238
+
239
+ ```sh
240
+ rake 'bootstrap:vendor[public/assets]'
241
+ rake 'bootstrap:version[public/assets]'
242
+ ```
243
+
244
+ The `.bootstrap-version` file and `vendor/` directories are created relative to the given path.
245
+
246
+ Tasks that accept `path`: `vendor`, `version`, `status`, `init`, `install`, `update`, `uninstall`.
247
+
248
+ For tasks that accept both `version` and `path` (`status`, `init`, `install`, `update`), `version` is the first positional arg. To pass only a path, leave version empty:
249
+
250
+ ```sh
251
+ rake 'bootstrap:install[5, public/assets]'
252
+ rake 'bootstrap:install[, public/assets]'
253
+ ```
254
+
255
+ ### Version constraint, what to install
256
+
257
+ `latest`, `status`, `init`, `install`, and `update` accept version constraints. The constraint finds the latest version within that major or minor:
258
+
259
+ | Constraint | Resolves to |
260
+ | ---------- | -------------------------- |
261
+ | `5` | Latest 5.x.y (e.g., 5.3.8) |
262
+ | `5.3` | Latest 5.3.x (e.g., 5.3.8) |
263
+ | `5.3.5` | Latest 5.3.x (e.g., 5.3.8) |
264
+ | `4` | Latest 4.x.y (e.g., 4.6.2) |
265
+
266
+ ## Requirements
267
+
268
+ - Ruby >= 4.0
269
+ - Rails (for automatic Railtie loading) or any Ruby project with Rake
270
+
271
+ ## License
272
+
273
+ MIT. See [LICENSE.txt](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ require 'http'
2
+ require 'json'
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new :spec
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
11
+
12
+ desc 'Update VERSIONS file from GitHub releases'
13
+ task :versions do
14
+ puts 'Fetching releases from GitHub...'
15
+
16
+ versions = []
17
+ page = 1
18
+
19
+ loop do
20
+ response = HTTP.get "https://api.github.com/repos/twbs/bootstrap/releases?per_page=100&page=#{page}"
21
+ releases = JSON.parse response.body.to_s
22
+
23
+ break if releases.empty?
24
+
25
+ releases.each do |release|
26
+ tag = release['tag_name'].delete_prefix 'v'
27
+ version = Gem::Version.new tag
28
+
29
+ next if version.prerelease?
30
+
31
+ versions << tag
32
+ end
33
+
34
+ page += 1
35
+ end
36
+
37
+ versions.sort_by! { Gem::Version.new it }
38
+
39
+ versions_path = File.expand_path 'VERSIONS.txt', __dir__
40
+ current = File.readlines(versions_path, chomp: true).reject(&:empty?)
41
+ new_versions = versions - current
42
+
43
+ if new_versions.empty?
44
+ puts 'VERSIONS is up to date.'
45
+ else
46
+ File.write versions_path, "#{versions.join "\n"}\n"
47
+
48
+ puts "Added #{new_versions.length} new version(s): #{new_versions.join ', '}"
49
+ end
50
+ end
data/VERSIONS.txt ADDED
@@ -0,0 +1,73 @@
1
+ 1.0.0
2
+ 1.1.0
3
+ 1.1.1
4
+ 1.2.0
5
+ 1.3.0
6
+ 1.4.0
7
+ 2.0.0
8
+ 2.0.1
9
+ 2.0.2
10
+ 2.0.3
11
+ 2.0.4
12
+ 2.1.0
13
+ 2.1.1
14
+ 2.2.0
15
+ 2.2.1
16
+ 2.2.2
17
+ 2.3.0
18
+ 2.3.1
19
+ 2.3.2
20
+ 3.0.0
21
+ 3.0.1
22
+ 3.0.2
23
+ 3.0.3
24
+ 3.1.0
25
+ 3.1.1
26
+ 3.2.0
27
+ 3.3.0
28
+ 3.3.1
29
+ 3.3.2
30
+ 3.3.4
31
+ 3.3.5
32
+ 3.3.6
33
+ 3.3.7
34
+ 3.4.0
35
+ 3.4.1
36
+ 4.0.0
37
+ 4.1.0
38
+ 4.1.1
39
+ 4.1.2
40
+ 4.1.3
41
+ 4.2.0
42
+ 4.2.1
43
+ 4.3.0
44
+ 4.3.1
45
+ 4.4.0
46
+ 4.4.1
47
+ 4.5.0
48
+ 4.5.1
49
+ 4.5.2
50
+ 4.5.3
51
+ 4.6.0
52
+ 4.6.1
53
+ 4.6.2
54
+ 5.0.0
55
+ 5.0.1
56
+ 5.0.2
57
+ 5.1.0
58
+ 5.1.1
59
+ 5.1.2
60
+ 5.1.3
61
+ 5.2.0
62
+ 5.2.1
63
+ 5.2.2
64
+ 5.2.3
65
+ 5.3.0
66
+ 5.3.1
67
+ 5.3.2
68
+ 5.3.3
69
+ 5.3.4
70
+ 5.3.5
71
+ 5.3.6
72
+ 5.3.7
73
+ 5.3.8
@@ -0,0 +1,46 @@
1
+ module Bootstrap
2
+ module Vendor
3
+ class Config
4
+ attr_reader :source
5
+
6
+ def initialize
7
+ @css_ltr = env_bool 'BOOTSTRAP_VENDOR_CSS_LTR', default: true
8
+ @css_map = env_bool 'BOOTSTRAP_VENDOR_CSS_MAP', default: true
9
+ @css_min = env_bool 'BOOTSTRAP_VENDOR_CSS_MIN', default: false
10
+ @css_rtl = env_bool 'BOOTSTRAP_VENDOR_CSS_RTL', default: false
11
+ @js_bundle = env_bool 'BOOTSTRAP_VENDOR_JS_BUNDLE', default: true
12
+ @js_map = env_bool 'BOOTSTRAP_VENDOR_JS_MAP', default: true
13
+ @js_min = env_bool 'BOOTSTRAP_VENDOR_JS_MIN', default: false
14
+ @source = ENV.fetch 'BOOTSTRAP_VENDOR_SOURCE', nil
15
+
16
+ validate!
17
+ end
18
+
19
+ def css_map? = @css_map
20
+ def css_min? = @css_min
21
+ def css_path = 'vendor/stylesheets'
22
+ def js_map? = @js_map
23
+ def js_min? = @js_min
24
+ def js_path = 'vendor/javascript'
25
+
26
+ def css_ltr? = @css_ltr
27
+ def css_rtl? = @css_rtl
28
+ def js_bundle? = @js_bundle
29
+
30
+ private
31
+
32
+ def env_bool key, default:
33
+ value = ENV.fetch key, nil
34
+ return default if value.nil?
35
+
36
+ value.downcase == 'true'
37
+ end
38
+
39
+ def validate!
40
+ return if css_ltr? || css_rtl?
41
+
42
+ raise Error, 'At least one of LTR or RTL must be true'
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,83 @@
1
+ module Bootstrap
2
+ module Vendor
3
+ class FileList
4
+ def initialize config:, version:, root: '.', sources: nil
5
+ @config = config
6
+ @version = version
7
+ @root = root
8
+ @sources = sources || Source.default_chain
9
+ end
10
+
11
+ def expected
12
+ css_entries + js_entries
13
+ end
14
+
15
+ def installed
16
+ expected.map { it[:destination] }.select { File.exist? it }
17
+ end
18
+
19
+ def download
20
+ dirs = expected.map { File.dirname it[:destination] }.uniq
21
+ dirs.each { FileUtils.mkdir_p it }
22
+
23
+ expected.each { download_with_fallback it }
24
+ end
25
+
26
+ private
27
+
28
+ def download_with_fallback entry
29
+ errors = []
30
+
31
+ @sources.each do |source|
32
+ source.download_file(
33
+ version: @version,
34
+ subdir: entry[:subdir],
35
+ filename: entry[:filename],
36
+ destination: entry[:destination]
37
+ )
38
+ errors.clear
39
+ break
40
+ rescue Down::Error, Zip::Error, IOError => e
41
+ errors << e
42
+ end
43
+
44
+ raise errors.last unless errors.empty?
45
+ end
46
+
47
+ def css_entries
48
+ filenames = []
49
+ filenames.concat(css_filenames_for(rtl: false)) if @config.css_ltr?
50
+ filenames.concat(css_filenames_for(rtl: true)) if @config.css_rtl?
51
+ filenames.map { build_entry(filename: it, subdir: 'css', local_path: @config.css_path) }
52
+ end
53
+
54
+ def css_filenames_for rtl:
55
+ base = rtl ? 'bootstrap.rtl' : 'bootstrap'
56
+ base = "#{base}.min" if @config.css_min?
57
+ names = ["#{base}.css"]
58
+ names << "#{base}.css.map" if @config.css_map?
59
+ names
60
+ end
61
+
62
+ def js_entries
63
+ js_filenames.map { build_entry(filename: it, subdir: 'js', local_path: @config.js_path) }
64
+ end
65
+
66
+ def js_filenames
67
+ base = @config.js_bundle? ? 'bootstrap.bundle' : 'bootstrap'
68
+ base = "#{base}.min" if @config.js_min?
69
+ names = ["#{base}.js"]
70
+ names << "#{base}.js.map" if @config.js_map?
71
+ names
72
+ end
73
+
74
+ def build_entry filename:, subdir:, local_path:
75
+ {
76
+ filename:,
77
+ subdir:,
78
+ destination: File.join(@root, local_path, filename)
79
+ }
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,9 @@
1
+ module Bootstrap
2
+ module Vendor
3
+ class Railtie < Rails::Railtie
4
+ rake_tasks do
5
+ load File.expand_path('../../tasks/bootstrap.rake', __dir__)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ module Bootstrap
2
+ module Vendor
3
+ module Registry
4
+ VERSIONS_PATH = File.expand_path('../../../VERSIONS.txt', __dir__).freeze
5
+
6
+ def self.all
7
+ @all ||= File.readlines(VERSIONS_PATH, chomp: true).reject(&:empty?)
8
+ end
9
+
10
+ def self.latest constraint: nil
11
+ return all.last if constraint.nil?
12
+
13
+ requirement = build_requirement(constraint.to_s)
14
+ matches = all.select { requirement.satisfied_by? Gem::Version.new it }
15
+
16
+ raise Error, "No versions matching '#{constraint}'" if matches.empty?
17
+
18
+ matches.last
19
+ end
20
+
21
+ def self.build_requirement constraint
22
+ segments = Gem::Version.new(constraint).segments
23
+
24
+ case segments.length
25
+ when 1 then Gem::Requirement.new("~> #{segments[0]}.0")
26
+ when 2, 3 then Gem::Requirement.new("~> #{segments[0]}.#{segments[1]}.0")
27
+ end
28
+ end
29
+
30
+ private_class_method :build_requirement
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ require 'zip'
2
+
3
+ module Bootstrap
4
+ module Vendor
5
+ module Source
6
+ class GitHubAPI
7
+ NAME = 'github_api'.freeze
8
+ BASE_URL = 'https://github.com/twbs/bootstrap/releases/download/v'.freeze
9
+
10
+ def name = NAME
11
+
12
+ def url_for version:, subdir: nil, filename: nil # rubocop:disable Lint/UnusedMethodArgument
13
+ [BASE_URL, version, '/bootstrap-', version, '-dist.zip'].join
14
+ end
15
+
16
+ def download_file version:, subdir:, filename:, destination:
17
+ zip = cached_zip(version:)
18
+ entry_path = "bootstrap-#{version}-dist/#{subdir}/#{filename}"
19
+ entry = zip.find_entry(entry_path)
20
+
21
+ raise Down::NotFound, "#{entry_path} not found in zip" unless entry
22
+
23
+ File.binwrite destination, entry.get_input_stream.read
24
+ end
25
+
26
+ private
27
+
28
+ def cached_zip version:
29
+ @cached_zips ||= {}
30
+ @cached_zips[version] ||= download_zip(version:)
31
+ end
32
+
33
+ def download_zip version:
34
+ tempfile = Down.download url_for(version:)
35
+ Zip::File.open tempfile.path
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ module Bootstrap
2
+ module Vendor
3
+ module Source
4
+ class GitHubRaw
5
+ NAME = 'github_raw'.freeze
6
+ BASE_URL = 'https://raw.githubusercontent.com/twbs/bootstrap/refs/tags/v'.freeze
7
+
8
+ def name = NAME
9
+
10
+ def url_for version:, subdir:, filename:
11
+ [BASE_URL, version, '/dist/', subdir, '/', filename].join
12
+ end
13
+
14
+ def download_file version:, subdir:, filename:, destination:
15
+ Down.download url_for(version:, subdir:, filename:), destination:
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Bootstrap
2
+ module Vendor
3
+ module Source
4
+ class JSDelivrGitHub
5
+ NAME = 'jsdelivr_github'.freeze
6
+ BASE_URL = 'https://cdn.jsdelivr.net/gh/twbs/bootstrap@v'.freeze
7
+
8
+ def name = NAME
9
+
10
+ def url_for version:, subdir:, filename:
11
+ [BASE_URL, version, '/dist/', subdir, '/', filename].join
12
+ end
13
+
14
+ def download_file version:, subdir:, filename:, destination:
15
+ Down.download url_for(version:, subdir:, filename:), destination:
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Bootstrap
2
+ module Vendor
3
+ module Source
4
+ class JSDelivrNPM
5
+ NAME = 'jsdelivr_npm'.freeze
6
+ BASE_URL = 'https://cdn.jsdelivr.net/npm/bootstrap@'.freeze
7
+
8
+ def name = NAME
9
+
10
+ def url_for version:, subdir:, filename:
11
+ [BASE_URL, version, '/dist/', subdir, '/', filename].join
12
+ end
13
+
14
+ def download_file version:, subdir:, filename:, destination:
15
+ Down.download url_for(version:, subdir:, filename:), destination:
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,30 @@
1
+ require_relative 'source/git_hub_api'
2
+ require_relative 'source/git_hub_raw'
3
+ require_relative 'source/js_delivr_git_hub'
4
+ require_relative 'source/js_delivr_npm'
5
+
6
+ module Bootstrap
7
+ module Vendor
8
+ module Source
9
+ SOURCES = {
10
+ jsdelivr_npm: JSDelivrNPM,
11
+ jsdelivr_github: JSDelivrGitHub,
12
+ github_raw: GitHubRaw,
13
+ github_api: GitHubAPI
14
+ }.freeze
15
+
16
+ DEFAULT_ORDER = %i[jsdelivr_npm jsdelivr_github github_raw github_api].freeze
17
+
18
+ def self.build name
19
+ klass = SOURCES[name.to_sym]
20
+ raise Error, "Unknown source: '#{name}'. Valid sources: #{SOURCES.keys.join(', ')}" unless klass
21
+
22
+ klass.new
23
+ end
24
+
25
+ def self.default_chain
26
+ DEFAULT_ORDER.map { SOURCES[it].new }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ module Bootstrap
2
+ module Vendor
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,42 @@
1
+ module Bootstrap
2
+ module Vendor
3
+ class VersionFile
4
+ FILENAME = '.bootstrap-version'.freeze
5
+
6
+ attr_reader :path
7
+
8
+ def initialize path:
9
+ @path = resolve_path path
10
+ end
11
+
12
+ def delete
13
+ File.delete @path if exists?
14
+ end
15
+
16
+ def exists?
17
+ File.exist? @path
18
+ end
19
+
20
+ def read
21
+ return nil unless exists?
22
+
23
+ File.read(@path).strip
24
+ end
25
+
26
+ def write version:
27
+ FileUtils.mkdir_p File.dirname(@path)
28
+ File.write @path, "#{version}\n"
29
+ end
30
+
31
+ private
32
+
33
+ def resolve_path path
34
+ if File.directory?(path) || !path.end_with?(FILENAME)
35
+ File.join path, FILENAME
36
+ else
37
+ path
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ require 'down'
2
+ require 'fileutils'
3
+ require 'tmpdir'
4
+
5
+ require_relative 'vendor/config'
6
+ require_relative 'vendor/file_list'
7
+ require_relative 'vendor/registry'
8
+ require_relative 'vendor/source'
9
+ require_relative 'vendor/version'
10
+ require_relative 'vendor/version_file'
11
+
12
+ module Bootstrap
13
+ module Vendor
14
+ class Error < StandardError; end
15
+ end
16
+ end
17
+
18
+ require_relative 'vendor/railtie' if defined? Rails::Railtie
@@ -0,0 +1,200 @@
1
+ require 'bootstrap/vendor'
2
+
3
+ def bootstrap_sources config
4
+ if config.source
5
+ [Bootstrap::Vendor::Source.build(config.source)]
6
+ else
7
+ Bootstrap::Vendor::Source.default_chain
8
+ end
9
+ end
10
+
11
+ namespace :bootstrap do
12
+ desc 'Print the current Bootstrap version from .bootstrap-version'
13
+ task :version, [:path] do |_t, args|
14
+ path = args[:path] || '.'
15
+ version_file = Bootstrap::Vendor::VersionFile.new(path:)
16
+
17
+ if version_file.exists?
18
+ puts version_file.read
19
+ else
20
+ puts 'No .bootstrap-version file found. Create one with:'
21
+ puts 'rake bootstrap:init'
22
+ end
23
+ end
24
+
25
+ desc 'Print the latest Bootstrap version (optionally within a constraint)'
26
+ task :latest, [:version] do |_t, args|
27
+ constraint = args[:version]
28
+ latest = Bootstrap::Vendor::Registry.latest(constraint:)
29
+
30
+ puts latest
31
+ end
32
+
33
+ desc 'Compare current version to latest upstream version'
34
+ task :status, [:version, :path] do |_t, args|
35
+ path = args[:path] || '.'
36
+ version_file = Bootstrap::Vendor::VersionFile.new(path:)
37
+
38
+ unless version_file.exists?
39
+ puts 'No .bootstrap-version file found. Create one with:'
40
+ puts 'rake bootstrap:init'
41
+ next
42
+ end
43
+
44
+ current = version_file.read
45
+ constraint = args[:version]
46
+ latest = Bootstrap::Vendor::Registry.latest(constraint:)
47
+
48
+ if current == latest
49
+ puts "Current version #{current} is up to date"
50
+ else
51
+ constraint_label = constraint ? " #{constraint}.x.y" : ''
52
+ puts "Current version #{current} is behind latest#{constraint_label} version #{latest}"
53
+ end
54
+ end
55
+
56
+ desc 'Create a .bootstrap-version file'
57
+ task :init, [:version, :path, :overwrite] do |_t, args|
58
+ version = args[:version]
59
+ path = args[:path] || '.'
60
+ overwrite = args[:overwrite] == 'overwrite' || version == 'overwrite'
61
+
62
+ # Handle bootstrap:init['overwrite'] (version arg is actually 'overwrite')
63
+ version = nil if version == 'overwrite'
64
+
65
+ resolved = Bootstrap::Vendor::Registry.latest constraint: version
66
+ version_file = Bootstrap::Vendor::VersionFile.new(path:)
67
+
68
+ if version_file.exists? && !overwrite
69
+ puts 'A .bootstrap-version file already exists.'
70
+ puts "Version: #{version_file.read}"
71
+ puts "Use 'overwrite' to replace it."
72
+ puts "rake bootstrap:init['overwrite']"
73
+ next
74
+ end
75
+
76
+ version_file.write version: resolved
77
+
78
+ puts 'A .bootstrap-version file was created.'
79
+ puts "Version: #{resolved}"
80
+ end
81
+
82
+ desc 'Download Bootstrap CSS and JS files'
83
+ task :install, [:version, :path] do |_t, args|
84
+ path = args[:path] || '.'
85
+ version_file = Bootstrap::Vendor::VersionFile.new(path:)
86
+ constraint = args[:version]
87
+ resolved = if version_file.exists? && constraint.nil?
88
+ version_file.read
89
+ else
90
+ Bootstrap::Vendor::Registry.latest(constraint:)
91
+ end
92
+
93
+ config = Bootstrap::Vendor::Config.new
94
+ sources = bootstrap_sources config
95
+ file_list = Bootstrap::Vendor::FileList.new(config:, version: resolved, root: path, sources:)
96
+
97
+ if file_list.installed.length == file_list.expected.length
98
+ puts 'Bootstrap is already installed.'
99
+ puts "Version: #{resolved}"
100
+ puts "Use 'rake bootstrap:update' to update."
101
+ next
102
+ end
103
+
104
+ file_list.download
105
+
106
+ version_file.write version: resolved
107
+
108
+ puts 'CSS:'
109
+ file_list.expected.select { it[:destination].include?('stylesheets') }.each do |entry|
110
+ puts " #{entry[:destination]}"
111
+ end
112
+ puts 'JS:'
113
+ file_list.expected.select { it[:destination].include?('javascript') }.each do |entry|
114
+ puts " #{entry[:destination]}"
115
+ end
116
+ end
117
+
118
+ desc 'Update Bootstrap CSS and JS files to latest version'
119
+ task :update, [:version, :path] do |_t, args|
120
+ path = args[:path] || '.'
121
+ version_file = Bootstrap::Vendor::VersionFile.new(path:)
122
+ current = version_file.read || '0.0.0'
123
+
124
+ constraint = args[:version]
125
+ resolved = Bootstrap::Vendor::Registry.latest constraint: constraint
126
+ config = Bootstrap::Vendor::Config.new
127
+ sources = bootstrap_sources config
128
+ file_list = Bootstrap::Vendor::FileList.new(config:, version: resolved, root: path, sources:)
129
+
130
+ file_list.download
131
+
132
+ version_file.write version: resolved
133
+
134
+ puts "Bootstrap updated from #{current} to #{resolved}"
135
+ puts ''
136
+ puts 'CSS:'
137
+ file_list.expected.select { it[:destination].include?('stylesheets') }.each do |entry|
138
+ puts " #{entry[:destination]}"
139
+ end
140
+ puts 'JS:'
141
+ file_list.expected.select { it[:destination].include?('javascript') }.each do |entry|
142
+ puts " #{entry[:destination]}"
143
+ end
144
+ end
145
+
146
+ desc 'Vendor Bootstrap: init, fetch latest, update'
147
+ task :vendor, [:path] do |_t, args|
148
+ path = args[:path] || '.'
149
+ version_file = Bootstrap::Vendor::VersionFile.new(path:)
150
+
151
+ resolved = Bootstrap::Vendor::Registry.latest
152
+ config = Bootstrap::Vendor::Config.new
153
+ sources = bootstrap_sources config
154
+ file_list = Bootstrap::Vendor::FileList.new(config:, version: resolved, root: path, sources:)
155
+
156
+ file_list.download
157
+
158
+ version_file.write(version: resolved)
159
+
160
+ puts "Bootstrap #{resolved} vendored."
161
+ puts ''
162
+ puts 'CSS:'
163
+ file_list.expected.select { it[:destination].include?('stylesheets') }.each do |entry|
164
+ puts " #{entry[:destination]}"
165
+ end
166
+ puts 'JS:'
167
+ file_list.expected.select { it[:destination].include?('javascript') }.each do |entry|
168
+ puts " #{entry[:destination]}"
169
+ end
170
+ end
171
+
172
+ desc 'Remove vendored Bootstrap files'
173
+ task :uninstall, [:path] do |_t, args|
174
+ path = args[:path] || '.'
175
+ version_file = Bootstrap::Vendor::VersionFile.new(path:)
176
+ current = version_file.read
177
+ config = Bootstrap::Vendor::Config.new
178
+ file_list = Bootstrap::Vendor::FileList.new config:, version: current || '0', root: path
179
+ deleted = false
180
+
181
+ file_list.installed.each do |destination|
182
+ File.delete destination
183
+ puts "Deleted #{destination}"
184
+ deleted = true
185
+ end
186
+
187
+ if version_file.exists?
188
+ version_file.delete
189
+ puts 'Deleted .bootstrap-version'
190
+ deleted = true
191
+ end
192
+
193
+ if deleted
194
+ message = current ? "Bootstrap #{current} uninstalled." : 'Bootstrap uninstalled.'
195
+ puts message
196
+ else
197
+ puts 'No Bootstrap installation found.'
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,6 @@
1
+ module Bootstrap
2
+ module Vendor
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bootstrap-vendor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shane Becker
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: down
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.5'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.5'
26
+ - !ruby/object:Gem::Dependency
27
+ name: http
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '6.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '6.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rubyzip
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.2'
54
+ description: 'Manage vendored Bootstrap files with rake tasks: check versions, download,
55
+ update, and uninstall.'
56
+ email:
57
+ - veganstraightedge@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - CHANGELOG.md
63
+ - CODE_OF_CONDUCT.md
64
+ - LICENSE.txt
65
+ - README.md
66
+ - Rakefile
67
+ - VERSIONS.txt
68
+ - lib/bootstrap/vendor.rb
69
+ - lib/bootstrap/vendor/config.rb
70
+ - lib/bootstrap/vendor/file_list.rb
71
+ - lib/bootstrap/vendor/railtie.rb
72
+ - lib/bootstrap/vendor/registry.rb
73
+ - lib/bootstrap/vendor/source.rb
74
+ - lib/bootstrap/vendor/source/git_hub_api.rb
75
+ - lib/bootstrap/vendor/source/git_hub_raw.rb
76
+ - lib/bootstrap/vendor/source/js_delivr_git_hub.rb
77
+ - lib/bootstrap/vendor/source/js_delivr_npm.rb
78
+ - lib/bootstrap/vendor/version.rb
79
+ - lib/bootstrap/vendor/version_file.rb
80
+ - lib/tasks/bootstrap.rake
81
+ - sig/bootstrap/vendor.rbs
82
+ homepage: https://github.com/xoengineering/bootstrap-vendor
83
+ licenses:
84
+ - MIT
85
+ metadata:
86
+ allowed_push_host: https://rubygems.org
87
+ homepage_uri: https://github.com/xoengineering/bootstrap-vendor
88
+ source_code_uri: https://github.com/xoengineering/bootstrap-vendor
89
+ changelog_uri: https://github.com/xoengineering/bootstrap-vendor/blob/main/CHANGELOG.md
90
+ rubygems_mfa_required: 'true'
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '4.0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubygems_version: 4.0.8
106
+ specification_version: 4
107
+ summary: Rake tasks to vendor Bootstrap CSS and JS into your Rails app
108
+ test_files: []