csspin-rails 1.0.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 +7 -0
- data/README.md +77 -0
- data/exe/csspin +5 -0
- data/lib/csspin/cli.rb +59 -0
- data/lib/csspin/commands/pin.rb +64 -0
- data/lib/csspin/css_fallback_candidates.rb +66 -0
- data/lib/csspin/http/downloader.rb +56 -0
- data/lib/csspin/instructions_printer.rb +17 -0
- data/lib/csspin/jsdelivr_metadata_client.rb +42 -0
- data/lib/csspin/package_spec.rb +47 -0
- data/lib/csspin/resolver/jsdelivr.rb +17 -0
- data/lib/csspin/vendor_writer.rb +24 -0
- data/lib/csspin/version.rb +5 -0
- data/lib/csspin.rb +12 -0
- metadata +102 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c1717d5c0156c805e78da55e7b3646c305586cc9d74b243d9799b29d76dd2e75
|
|
4
|
+
data.tar.gz: 6fa180b07893e1c5a119da73c80318eacd7663791ae2c6c149c46031fe8cde45
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 827c66a027c92c20c3e236596903b70cf462eadd8b081dcd491cdb60129791e88b004dfe98576ab8b192a17a90787e766397d3d8e72921f2133e90e67dad4229
|
|
7
|
+
data.tar.gz: 7a04c7c035d9fa58e151c0c4af7456b9ce0c7923c78495de426dbef7bb99e63ac5310481ac071a0c681d998d77c45a6313c9e4f7e3aebc3867a11600b033c219
|
data/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# csspin-rails
|
|
2
|
+
|
|
3
|
+
[](https://github.com/elalemanyo/csspin-rails/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Pin CSS packages from npm into a Rails app's `vendor/assets/stylesheets` — without Node.js.
|
|
6
|
+
|
|
7
|
+
Works like [importmap-rails](https://github.com/rails/importmap-rails), but for CSS. Packages are fetched from [jsDelivr](https://www.jsdelivr.com/).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Add to your Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "csspin-rails"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then run:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bundle install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Pin a package (latest version)
|
|
27
|
+
bundle exec csspin pin trix
|
|
28
|
+
|
|
29
|
+
# Pin a specific version
|
|
30
|
+
bundle exec csspin pin trix@2.0.0
|
|
31
|
+
|
|
32
|
+
# Pin a scoped package
|
|
33
|
+
bundle exec csspin pin @scope/package@1.2.3
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
CSS is saved to `vendor/assets/stylesheets/<package>.css`.
|
|
37
|
+
|
|
38
|
+
If you want `bin/csspin`, run `bundle binstubs csspin-rails` first.
|
|
39
|
+
|
|
40
|
+
After pinning, the CLI prints integration snippets:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
Sprockets snippet: *= require trix
|
|
44
|
+
Sass snippet: @import "trix";
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## CLI
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Usage: csspin pin <package|package@version>
|
|
51
|
+
|
|
52
|
+
Commands:
|
|
53
|
+
pin <package|package@version> Download a CSS package into vendor/assets/stylesheets
|
|
54
|
+
|
|
55
|
+
Options:
|
|
56
|
+
-v, --version Show version
|
|
57
|
+
-h, --help Show this help
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## How it works
|
|
61
|
+
|
|
62
|
+
1. Parses the package name and optional version
|
|
63
|
+
2. Resolves candidate URLs on jsDelivr (`/dist/<pkg>.css`, then the bare package URL)
|
|
64
|
+
3. Downloads the first successful CSS response (follows redirects, validates content-type)
|
|
65
|
+
4. Writes the file to `vendor/assets/stylesheets/`
|
|
66
|
+
|
|
67
|
+
## Development
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
bundle install
|
|
71
|
+
bundle exec standardrb
|
|
72
|
+
bundle exec rake test
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/exe/csspin
ADDED
data/lib/csspin/cli.rb
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csspin
|
|
4
|
+
class CLI
|
|
5
|
+
USAGE = "Usage: csspin pin <package|package@version>"
|
|
6
|
+
|
|
7
|
+
def self.run(argv:, root: Dir.pwd, io: $stdout, error_io: $stderr, pin_command: nil)
|
|
8
|
+
command = argv[0]
|
|
9
|
+
|
|
10
|
+
if command == "--version" || command == "-v"
|
|
11
|
+
io.puts "csspin-rails #{Csspin::VERSION}"
|
|
12
|
+
return 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
if command == "--help" || command == "-h" || command.nil?
|
|
16
|
+
io.puts USAGE
|
|
17
|
+
io.puts
|
|
18
|
+
io.puts "Commands:"
|
|
19
|
+
io.puts " pin <package|package@version> Download a CSS package into vendor/assets/stylesheets"
|
|
20
|
+
io.puts
|
|
21
|
+
io.puts "Options:"
|
|
22
|
+
io.puts " -v, --version Show version"
|
|
23
|
+
io.puts " -h, --help Show this help"
|
|
24
|
+
return 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
unless command == "pin"
|
|
28
|
+
error_io.puts "Unknown command: #{command}"
|
|
29
|
+
error_io.puts USAGE
|
|
30
|
+
return 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
input = argv[1]
|
|
34
|
+
if input.to_s.strip.empty?
|
|
35
|
+
error_io.puts USAGE
|
|
36
|
+
return 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
downloader = Csspin::HTTP::Downloader.new
|
|
40
|
+
|
|
41
|
+
cmd = pin_command || Csspin::Commands::Pin.new(
|
|
42
|
+
resolver: Csspin::Resolver::Jsdelivr.new,
|
|
43
|
+
downloader: downloader,
|
|
44
|
+
fallback_candidates: Csspin::CssFallbackCandidates.new(
|
|
45
|
+
metadata_client: Csspin::JsdelivrMetadataClient.new(downloader: downloader)
|
|
46
|
+
),
|
|
47
|
+
writer: Csspin::VendorWriter.new(root: root),
|
|
48
|
+
printer: Csspin::InstructionsPrinter.new(io: io),
|
|
49
|
+
error_io: error_io
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
cmd.run(input: input, root: root, io: io)
|
|
53
|
+
rescue ArgumentError => e
|
|
54
|
+
error_io.puts e.message
|
|
55
|
+
error_io.puts USAGE
|
|
56
|
+
1
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csspin
|
|
4
|
+
module Commands
|
|
5
|
+
class Pin
|
|
6
|
+
def initialize(resolver:, downloader:, writer:, printer:, fallback_candidates:, error_io: $stderr)
|
|
7
|
+
@resolver = resolver
|
|
8
|
+
@downloader = downloader
|
|
9
|
+
@writer = writer
|
|
10
|
+
@printer = printer
|
|
11
|
+
@error_io = error_io
|
|
12
|
+
@fallback_candidates = fallback_candidates
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run(input:, root:, io:)
|
|
16
|
+
spec = Csspin::PackageSpec.parse(input)
|
|
17
|
+
|
|
18
|
+
urls = @resolver.url_candidates_for(spec)
|
|
19
|
+
tried_urls = []
|
|
20
|
+
result = download_first_success(urls, tried_urls)
|
|
21
|
+
|
|
22
|
+
unless result
|
|
23
|
+
fallback_urls = @fallback_candidates.for(spec).reject { |url| tried_urls.include?(url) }
|
|
24
|
+
result = download_first_success(fallback_urls, tried_urls)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
unless result
|
|
28
|
+
@error_io.puts "Unable to download CSS from jsDelivr for #{spec.full_name}"
|
|
29
|
+
@error_io.puts "Tried: #{tried_urls.join(", ")}"
|
|
30
|
+
return 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
saved_path = @writer.write(package_name: spec.package_name, content: result[:body])
|
|
34
|
+
|
|
35
|
+
snippet_package = File.basename(saved_path, ".css")
|
|
36
|
+
|
|
37
|
+
@printer.print_success(
|
|
38
|
+
package_name: snippet_package,
|
|
39
|
+
saved_path: saved_path,
|
|
40
|
+
source_url: result[:url]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
0
|
|
44
|
+
rescue ArgumentError => e
|
|
45
|
+
@error_io.puts e.message
|
|
46
|
+
1
|
|
47
|
+
rescue => e
|
|
48
|
+
@error_io.puts "Pin failed: #{e.message}"
|
|
49
|
+
1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def download_first_success(urls, tried_urls)
|
|
55
|
+
urls.each do |url|
|
|
56
|
+
tried_urls << url
|
|
57
|
+
response = @downloader.get(url)
|
|
58
|
+
return response.merge(url: url) if response[:ok]
|
|
59
|
+
end
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csspin
|
|
4
|
+
class CssFallbackCandidates
|
|
5
|
+
def initialize(metadata_client:)
|
|
6
|
+
@metadata_client = metadata_client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def for(package_spec)
|
|
10
|
+
metadata = @metadata_client.fetch(package_spec.full_name)
|
|
11
|
+
|
|
12
|
+
[
|
|
13
|
+
metadata_path_for(metadata, "style"),
|
|
14
|
+
metadata_path_for(metadata, "default"),
|
|
15
|
+
*css_paths_for(metadata),
|
|
16
|
+
conventional_path_for(package_spec, "dist/css/"),
|
|
17
|
+
conventional_path_for(package_spec)
|
|
18
|
+
].compact.map { |path| normalize_path(path, package_spec.full_name) }
|
|
19
|
+
.uniq
|
|
20
|
+
.map { |path| candidate_url_for(path, package_spec.full_name) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def metadata_path_for(metadata, key)
|
|
26
|
+
path = metadata[key]
|
|
27
|
+
path if path&.end_with?(".css")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def conventional_path_for(package_spec, prefix = "")
|
|
31
|
+
"#{prefix}#{package_spec.package_name}.min.css"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def css_paths_for(metadata)
|
|
35
|
+
css_paths_from_entries(metadata["files"])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def css_paths_from_entries(entries, prefix = nil)
|
|
39
|
+
Array(entries).each_with_object([]) do |entry, paths|
|
|
40
|
+
next unless entry.is_a?(Hash)
|
|
41
|
+
|
|
42
|
+
name = entry["name"].to_s
|
|
43
|
+
|
|
44
|
+
case entry["type"]
|
|
45
|
+
when "directory"
|
|
46
|
+
path = [prefix, name].compact.join("/")
|
|
47
|
+
paths.concat(css_paths_from_entries(entry["files"], path))
|
|
48
|
+
when "file"
|
|
49
|
+
next unless name.end_with?(".css")
|
|
50
|
+
|
|
51
|
+
paths << [prefix, name].compact.join("/")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def normalize_path(path, full_name)
|
|
57
|
+
path.sub("https://cdn.jsdelivr.net/npm/#{full_name}/", "")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def candidate_url_for(path, full_name)
|
|
61
|
+
return path if path.start_with?("https://cdn.jsdelivr.net/npm/")
|
|
62
|
+
|
|
63
|
+
"https://cdn.jsdelivr.net/npm/#{full_name}/#{path}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Csspin
|
|
7
|
+
module HTTP
|
|
8
|
+
class Downloader
|
|
9
|
+
MAX_REDIRECTS = 5
|
|
10
|
+
DEFAULT_ALLOWED_CONTENT_TYPES = ["text/css", "text/plain"].freeze
|
|
11
|
+
|
|
12
|
+
def get(url, allowed_content_types: DEFAULT_ALLOWED_CONTENT_TYPES)
|
|
13
|
+
uri = URI.parse(url)
|
|
14
|
+
redirects = 0
|
|
15
|
+
|
|
16
|
+
loop do
|
|
17
|
+
http = build_http(uri)
|
|
18
|
+
response = http.start { |h| h.get(uri.request_uri) }
|
|
19
|
+
|
|
20
|
+
if response.is_a?(Net::HTTPRedirection)
|
|
21
|
+
redirects += 1
|
|
22
|
+
return {ok: false, error: "Too many redirects", status: response.code.to_i} if redirects > MAX_REDIRECTS
|
|
23
|
+
|
|
24
|
+
location = response["location"]
|
|
25
|
+
uri = URI.join(uri, location)
|
|
26
|
+
next
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
30
|
+
content_type = response["content-type"].to_s
|
|
31
|
+
unless allowed_content_types.any? { |allowed_content_type| content_type.include?(allowed_content_type) }
|
|
32
|
+
return {ok: false, error: "Not a CSS file (content-type: #{content_type})", status: response.code.to_i}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
return {ok: true, body: response.body}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
return {ok: false, error: "HTTP #{response.code}", status: response.code.to_i}
|
|
39
|
+
end
|
|
40
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNRESET, SocketError, URI::InvalidURIError => e
|
|
41
|
+
{ok: false, error: e.message, status: nil}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def build_http(uri)
|
|
47
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
48
|
+
http.use_ssl = uri.scheme == "https"
|
|
49
|
+
http.open_timeout = 5
|
|
50
|
+
http.read_timeout = 10
|
|
51
|
+
http.write_timeout = 5
|
|
52
|
+
http
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csspin
|
|
4
|
+
class InstructionsPrinter
|
|
5
|
+
def initialize(io: $stdout)
|
|
6
|
+
@io = io
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def print_success(package_name:, saved_path:, source_url:)
|
|
10
|
+
@io.puts "Saved CSS to: #{saved_path}"
|
|
11
|
+
@io.puts "Source URL: #{source_url}"
|
|
12
|
+
@io.puts
|
|
13
|
+
@io.puts "Sprockets snippet: *= require #{package_name}"
|
|
14
|
+
@io.puts "Sass snippet: @import \"#{package_name}\";"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Csspin
|
|
6
|
+
class JsdelivrMetadataClient
|
|
7
|
+
def initialize(downloader:)
|
|
8
|
+
@downloader = downloader
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def fetch(full_name)
|
|
12
|
+
metadata = fetch_json(full_name)
|
|
13
|
+
return {} unless metadata
|
|
14
|
+
|
|
15
|
+
if !metadata.key?("files") && (latest = resolve_latest_version(metadata))
|
|
16
|
+
versioned = fetch_json("#{full_name}@#{latest}")
|
|
17
|
+
return versioned if versioned
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
metadata
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def fetch_json(full_name)
|
|
26
|
+
response = @downloader.get(
|
|
27
|
+
"https://data.jsdelivr.com/v1/packages/npm/#{full_name}",
|
|
28
|
+
allowed_content_types: ["application/json", "text/json"]
|
|
29
|
+
)
|
|
30
|
+
return nil unless response[:ok]
|
|
31
|
+
|
|
32
|
+
metadata = JSON.parse(response[:body])
|
|
33
|
+
metadata.is_a?(Hash) ? metadata : nil
|
|
34
|
+
rescue JSON::ParserError, TypeError
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def resolve_latest_version(metadata)
|
|
39
|
+
metadata.dig("tags", "latest")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csspin
|
|
4
|
+
class PackageSpec
|
|
5
|
+
attr_reader :full_name, :package_name
|
|
6
|
+
|
|
7
|
+
def self.parse(raw)
|
|
8
|
+
normalized = raw.to_s.strip
|
|
9
|
+
raise ArgumentError, "package is required" if normalized.empty?
|
|
10
|
+
|
|
11
|
+
package_part, = split_package_and_version(normalized)
|
|
12
|
+
|
|
13
|
+
package_name = package_part.split("/").last
|
|
14
|
+
new(full_name: normalized, package_name: package_name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.split_package_and_version(normalized)
|
|
18
|
+
return validate_unscoped_package!(normalized) unless normalized.include?("@")
|
|
19
|
+
|
|
20
|
+
if normalized.start_with?("@")
|
|
21
|
+
scope, remainder = normalized.split("/", 2)
|
|
22
|
+
raise ArgumentError, "invalid package format: #{normalized}" if scope.nil? || scope.length == 1 || scope.count("@") != 1 || remainder.nil? || remainder.empty?
|
|
23
|
+
|
|
24
|
+
package, version = remainder.split("@", 2)
|
|
25
|
+
raise ArgumentError, "invalid package format: #{normalized}" if package.nil? || package.empty? || package.include?("/") || version&.empty? || version&.include?("/") || version&.include?("@")
|
|
26
|
+
|
|
27
|
+
["#{scope}/#{package}", version]
|
|
28
|
+
else
|
|
29
|
+
package, version = normalized.split("@", 2)
|
|
30
|
+
raise ArgumentError, "invalid package format: #{normalized}" if package.nil? || package.empty? || package.include?("/") || version&.empty? || version&.include?("/") || version&.include?("@")
|
|
31
|
+
|
|
32
|
+
[package, version]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.validate_unscoped_package!(normalized)
|
|
37
|
+
raise ArgumentError, "invalid package format: #{normalized}" if normalized.empty? || normalized.include?("/")
|
|
38
|
+
|
|
39
|
+
[normalized, nil]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def initialize(full_name:, package_name:)
|
|
43
|
+
@full_name = full_name
|
|
44
|
+
@package_name = package_name
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Csspin
|
|
4
|
+
module Resolver
|
|
5
|
+
class Jsdelivr
|
|
6
|
+
def url_candidates_for(package_spec)
|
|
7
|
+
full = package_spec.full_name
|
|
8
|
+
package = package_spec.package_name
|
|
9
|
+
|
|
10
|
+
[
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/#{full}/dist/#{package}.css",
|
|
12
|
+
"https://cdn.jsdelivr.net/npm/#{full}"
|
|
13
|
+
]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Csspin
|
|
6
|
+
class VendorWriter
|
|
7
|
+
def initialize(root: Dir.pwd)
|
|
8
|
+
@root = root
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def write(package_name:, content:)
|
|
12
|
+
package_name = package_name.tr("/", "-").gsub(/[^a-zA-Z0-9\-_]/, "")
|
|
13
|
+
|
|
14
|
+
dir = File.join(@root, "vendor/assets/stylesheets")
|
|
15
|
+
FileUtils.mkdir_p(dir)
|
|
16
|
+
|
|
17
|
+
path = File.join(dir, "#{package_name}.css")
|
|
18
|
+
File.write(path, content)
|
|
19
|
+
path
|
|
20
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
21
|
+
raise "Failed to write vendor file: #{e.message}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/csspin.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "csspin/version"
|
|
4
|
+
require_relative "csspin/package_spec"
|
|
5
|
+
require_relative "csspin/css_fallback_candidates"
|
|
6
|
+
require_relative "csspin/jsdelivr_metadata_client"
|
|
7
|
+
require_relative "csspin/resolver/jsdelivr"
|
|
8
|
+
require_relative "csspin/http/downloader"
|
|
9
|
+
require_relative "csspin/vendor_writer"
|
|
10
|
+
require_relative "csspin/instructions_printer"
|
|
11
|
+
require_relative "csspin/commands/pin"
|
|
12
|
+
require_relative "csspin/cli"
|
metadata
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: csspin-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- elalemanyo
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-23 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rake
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: minitest
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: standard
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
description: A CLI tool to pin CSS packages from npm (via jsDelivr) into a Rails app's
|
|
56
|
+
vendor/assets/stylesheets without Node.js.
|
|
57
|
+
email:
|
|
58
|
+
executables:
|
|
59
|
+
- csspin
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- README.md
|
|
64
|
+
- exe/csspin
|
|
65
|
+
- lib/csspin.rb
|
|
66
|
+
- lib/csspin/cli.rb
|
|
67
|
+
- lib/csspin/commands/pin.rb
|
|
68
|
+
- lib/csspin/css_fallback_candidates.rb
|
|
69
|
+
- lib/csspin/http/downloader.rb
|
|
70
|
+
- lib/csspin/instructions_printer.rb
|
|
71
|
+
- lib/csspin/jsdelivr_metadata_client.rb
|
|
72
|
+
- lib/csspin/package_spec.rb
|
|
73
|
+
- lib/csspin/resolver/jsdelivr.rb
|
|
74
|
+
- lib/csspin/vendor_writer.rb
|
|
75
|
+
- lib/csspin/version.rb
|
|
76
|
+
homepage: https://github.com/elalemanyo/csspin-rails
|
|
77
|
+
licenses:
|
|
78
|
+
- MIT
|
|
79
|
+
metadata:
|
|
80
|
+
homepage_uri: https://github.com/elalemanyo/csspin-rails
|
|
81
|
+
source_code_uri: https://github.com/elalemanyo/csspin-rails
|
|
82
|
+
rubygems_mfa_required: 'true'
|
|
83
|
+
post_install_message:
|
|
84
|
+
rdoc_options: []
|
|
85
|
+
require_paths:
|
|
86
|
+
- lib
|
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">="
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: '3.1'
|
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
requirements: []
|
|
98
|
+
rubygems_version: 3.5.22
|
|
99
|
+
signing_key:
|
|
100
|
+
specification_version: 4
|
|
101
|
+
summary: Pin CSS dependencies into Rails vendor assets
|
|
102
|
+
test_files: []
|