Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download all the files associated with a package from a CDN #235

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
8 changes: 4 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,13 @@ GEM
mini_mime (1.1.2)
minitest (5.16.2)
nio4r (2.5.8)
nokogiri (1.14.0-aarch64-linux)
nokogiri (1.16.0-aarch64-linux)
racc (~> 1.4)
nokogiri (1.14.0-arm64-darwin)
nokogiri (1.16.0-arm64-darwin)
racc (~> 1.4)
nokogiri (1.14.0-x86_64-darwin)
nokogiri (1.16.0-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.14.0-x86_64-linux)
nokogiri (1.16.0-x86_64-linux)
racc (~> 1.4)
public_suffix (4.0.7)
racc (1.6.2)
Expand Down
21 changes: 7 additions & 14 deletions lib/importmap/commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,9 @@ def self.exit_on_failure?
option :from, type: :string, aliases: :f, default: "jspm"
def pin(*packages)
if imports = packager.import(*packages, env: options[:env], from: options[:from])
imports.each do |package, url|
puts %(Pinning "#{package}" to #{packager.vendor_path}/#{package}.js via download from #{url})
packager.download(package, url)
pin = packager.vendored_pin_for(package, url)

if packager.packaged?(package)
gsub_file("config/importmap.rb", /^pin "#{package}".*$/, pin, verbose: false)
else
append_to_file("config/importmap.rb", "#{pin}\n", verbose: false)
end
imports.each do |package|
puts %(Pinning "#{package.package_name}" to #{packager.vendor_path}/#{package.vendored_package_folder} via download from #{package.base_url})
package.download
end
else
puts "Couldn't find any packages in #{packages.inspect} on #{options[:from]}"
Expand All @@ -35,10 +28,10 @@ def pin(*packages)
option :from, type: :string, aliases: :f, default: "jspm"
def unpin(*packages)
if imports = packager.import(*packages, env: options[:env], from: options[:from])
imports.each do |package, url|
if packager.packaged?(package)
puts %(Unpinning and removing "#{package}")
packager.remove(package)
imports.each do |package|
if packager.packaged?(package.package_name)
puts %(Unpinning and removing "#{package.package_name}")
package.remove
end
end
else
Expand Down
91 changes: 91 additions & 0 deletions lib/importmap/jspm_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
class Importmap::JspmApi
Error = Class.new(StandardError)
HTTPError = Class.new(Error)
ServiceError = Error.new(Error)

singleton_class.attr_accessor :generate_endpoint, :download_endpoint
self.generate_endpoint = "https://api.jspm.io/generate"
self.download_endpoint = "https://api.jspm.io/download"

def generate(install:, flatten_scope:, env:, provider:)
response = post_json(self.class.generate_endpoint, {
install:,
flattenScope: flatten_scope,
env:,
provider: normalize_provider(provider)
})

response_json(response)
end

def download(versioned_package_names:, provider:, exclude:)
response = post_json("#{self.class.download_endpoint}", {
packages: versioned_package_names,
provider: normalize_provider(provider),
exclude:
})

json = response_json(response)

return {} if json.blank?

json.transform_values do |package_download_details|
files = package_download_details["files"]
package_uri = URI(package_download_details["pkgUrl"])

Net::HTTP.start(package_uri.hostname, { use_ssl: true }) do |http|
files.map do |file|
[
file,
fetch_file(http, "#{package_uri.path}/#{file}")
]
end.to_h
end
end
end

private
def fetch_file(http, path)
response = http.get(path)

if response.code == "200"
response.body
else
handle_failure_response(response)
end
rescue => error
raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})"
end

def response_json(response)
case response.code
when "200" then JSON.parse(response.body)
when "404", "401" then {}
else handle_failure_response(response)
end
end

def normalize_provider(name)
name.to_s == "jspm" ? "jspm.io" : name.to_s
end

def post_json(endpoint, body)
Net::HTTP.post(URI(endpoint), body.to_json, "Content-Type" => "application/json")
rescue => error
raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})"
end

def handle_failure_response(response)
if error_message = parse_service_error(response)
raise ServiceError, error_message
else
raise HTTPError, "Unexpected response code (#{response.code})"
end
end

def parse_service_error(response)
JSON.parse(response.body.to_s)["error"]
rescue JSON::ParserError
nil
end
end
116 changes: 116 additions & 0 deletions lib/importmap/package.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
require "importmap/jspm_api"

class Importmap::Package
attr_reader :base_url, :main_url, :package_name

def initialize(
package_name:,
main_url:,
packager:,
provider:
)
@package_name = package_name
@main_url = main_url
@packager = packager
@provider = provider

@base_url = extract_base_url_from(main_url)
@version = extract_package_version_from(@main_url)

@main_file = main_url[(base_url.size + 1)..]
end

def download
@packager.ensure_vendor_directory_exists
remove_existing_package_files

jspm_api = Importmap::JspmApi.new

versioned_package_name = "#{@package_name}#{@version}"

files = jspm_api.download(
versioned_package_names: [versioned_package_name],
provider: @provider,
exclude: ["unused", "readme", "types"]
)[versioned_package_name]

files.each do |file, downloaded_file|
save_vendored_file(file, downloaded_file)
end

@packager.pin_package_in_importmap(@package_name, vendored_pin)
end

def remove
remove_existing_package_files
@packager.remove_package_from_importmap(@package_name)
end

def vendored_package_folder
@packager.vendor_path.join(folder_name)
end

private
def vendored_pin
filename = "#{folder_name}/#{@main_file}"

%(pin "#{package_name}", to: "#{filename}" # #{@version})
end

def save_vendored_file(file, source)
file_name = vendored_package_path_for_file(file)
ensure_parent_directories_exist_for(file_name)

File.open(file_name, "w+") do |vendored_file|
vendored_file.write "// #{@package_name}#{@version}/#{file} downloaded from #{base_url}/#{file}\n\n"

vendored_file.write remove_sourcemap_comment_from(source).force_encoding("UTF-8")
end
end

def ensure_parent_directories_exist_for(file)
dir_name = File.dirname(file)

unless File.directory?(dir_name)
FileUtils.mkdir_p(dir_name)
end
end

def remove_sourcemap_comment_from(source)
source.gsub(/^\/\/# sourceMappingURL=.*/, "")
end

def vendored_package_path_for_file(file)
vendored_package_folder.join(file)
end

def handle_failure_response(response)
if error_message = parse_service_error(response)
raise ServiceError, error_message
else
raise HTTPError, "Unexpected response code (#{response.code})"
end
end

def parse_service_error(response)
JSON.parse(response.body.to_s)["error"]
rescue JSON::ParserError
nil
end

def remove_existing_package_files
FileUtils.rm_rf vendored_package_folder
end

def folder_name
@package_name.gsub("/", "--")
end

def extract_base_url_from(url)
url.match(/^.+@\d+\.\d+\.\d+/)&.to_a&.first
end

def extract_package_version_from(url)
url.match(/@\d+\.\d+\.\d+/)&.to_a&.first
end
end
Loading