importmap 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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +6 -0
- data/Guardfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +20 -0
- data/Rakefile +6 -0
- data/exe/importmap +14 -0
- data/importmap.gemspec +32 -0
- data/lib/importmap/autoloader.rb +22 -0
- data/lib/importmap/cli/help/completion.md +20 -0
- data/lib/importmap/cli/help/completion_script.md +3 -0
- data/lib/importmap/cli/help/pin.md +5 -0
- data/lib/importmap/cli/help.rb +11 -0
- data/lib/importmap/cli.rb +156 -0
- data/lib/importmap/command.rb +89 -0
- data/lib/importmap/completer/script.rb +8 -0
- data/lib/importmap/completer/script.sh +10 -0
- data/lib/importmap/completer.rb +158 -0
- data/lib/importmap/framework/jets_framework.rb +11 -0
- data/lib/importmap/framework/rails_framework.rb +11 -0
- data/lib/importmap/framework.rb +49 -0
- data/lib/importmap/map.rb +158 -0
- data/lib/importmap/npm.rb +124 -0
- data/lib/importmap/packager.rb +145 -0
- data/lib/importmap/reloader.rb +20 -0
- data/lib/importmap/version.rb +3 -0
- data/lib/importmap.rb +24 -0
- data/spec/cli_spec.rb +26 -0
- data/spec/spec_helper.rb +29 -0
- metadata +217 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
require "active_support/core_ext/string/inflections"
|
2
|
+
|
3
|
+
module Importmap
|
4
|
+
class Framework
|
5
|
+
|
6
|
+
@@instance = nil
|
7
|
+
def self.instance
|
8
|
+
@@instance ||= new
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
require framework_name
|
13
|
+
Importmap::Framework.send(:include, include_framework)
|
14
|
+
end
|
15
|
+
|
16
|
+
def framework_name
|
17
|
+
ENV['IMPORTMAP_FRAMEWORK'] || infer_from_gemfile || "jets"
|
18
|
+
end
|
19
|
+
|
20
|
+
# define at bottom so methods take precedence
|
21
|
+
def include_framework
|
22
|
+
if supported_frameworks.include?(framework_name)
|
23
|
+
"Importmap::Framework::#{framework_name.camelize}Framework".constantize
|
24
|
+
else
|
25
|
+
raise "Need to use a supported framework: #{supported_frameworks.join(', ')}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def infer_from_gemfile
|
30
|
+
return unless File.exist?("Gemfile")
|
31
|
+
lines = IO.readlines("Gemfile")
|
32
|
+
lines.each do |line|
|
33
|
+
next if line =~ /^\s*#/ # skip comments
|
34
|
+
gem_name = line.match(/gem ['"](\w+)['"]/)&.captures&.first
|
35
|
+
if supported_frameworks.include?(gem_name)
|
36
|
+
return gem_name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
# To add another framework:
|
43
|
+
# 1. add to this list
|
44
|
+
# 2. define a module in importmap/framework/#{framework_name}_framework.rb
|
45
|
+
def supported_frameworks
|
46
|
+
%w[jets rails]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require "pathname"
|
2
|
+
|
3
|
+
class Importmap::Map
|
4
|
+
attr_reader :packages, :directories
|
5
|
+
|
6
|
+
class InvalidFile < StandardError; end
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@packages, @directories = {}, {}
|
10
|
+
@cache = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def draw(path = nil, &block)
|
14
|
+
if path && File.exist?(path)
|
15
|
+
begin
|
16
|
+
instance_eval(File.read(path), path.to_s)
|
17
|
+
rescue StandardError => e
|
18
|
+
Importmap.framework.logger.error "Unable to parse import map from #{path}: #{e.message}"
|
19
|
+
raise InvalidFile, "Unable to parse import map from #{path}: #{e.message}"
|
20
|
+
end
|
21
|
+
elsif block_given?
|
22
|
+
instance_eval(&block)
|
23
|
+
end
|
24
|
+
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def pin(name, to: nil, preload: false)
|
29
|
+
clear_cache
|
30
|
+
@packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload)
|
31
|
+
end
|
32
|
+
|
33
|
+
def pin_all_from(dir, under: nil, to: nil, preload: false)
|
34
|
+
clear_cache
|
35
|
+
@directories[dir] = MappedDir.new(dir: dir, under: under, path: to, preload: preload)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns an array of all the resolved module paths of the pinned packages. The `resolver` must respond to
|
39
|
+
# `path_to_asset`, such as `ActionController::Base.helpers` or `ApplicationController.helpers`. You'll want to use the
|
40
|
+
# resolver that has been configured for the `asset_host` you want these resolved paths to use. In case you need to
|
41
|
+
# resolve for different asset hosts, you can pass in a custom `cache_key` to vary the cache used by this method for
|
42
|
+
# the different cases.
|
43
|
+
def preloaded_module_paths(resolver:, cache_key: :preloaded_module_paths)
|
44
|
+
cache_as(cache_key) do
|
45
|
+
resolve_asset_paths(expanded_preloading_packages_and_directories, resolver: resolver).values
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns a JSON hash (as a string) of all the resolved module paths of the pinned packages in the import map format.
|
50
|
+
# The `resolver` must respond to `path_to_asset`, such as `ActionController::Base.helpers` or
|
51
|
+
# `ApplicationController.helpers`. You'll want to use the resolver that has been configured for the `asset_host` you
|
52
|
+
# want these resolved paths to use. In case you need to resolve for different asset hosts, you can pass in a custom
|
53
|
+
# `cache_key` to vary the cache used by this method for the different cases.
|
54
|
+
def to_json(resolver:, cache_key: :json)
|
55
|
+
cache_as(cache_key) do
|
56
|
+
JSON.pretty_generate({ "imports" => resolve_asset_paths(expanded_packages_and_directories, resolver: resolver) })
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns a SHA1 digest of the import map json that can be used as a part of a page etag to
|
61
|
+
# ensure that a html cache is invalidated when the import map is changed.
|
62
|
+
#
|
63
|
+
# Example:
|
64
|
+
#
|
65
|
+
# class ApplicationController < ActionController::Base
|
66
|
+
# etag { Importmap.framework.application.importmap.digest(resolver: helpers) if request.format&.html? }
|
67
|
+
# end
|
68
|
+
def digest(resolver:)
|
69
|
+
Digest::SHA1.hexdigest(to_json(resolver: resolver).to_s)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns an instance ActiveSupport::EventedFileUpdateChecker configured to clear the cache of the map
|
73
|
+
# when the directories passed on initialization via `watches:` have changes. This is used in development
|
74
|
+
# and test to ensure the map caches are reset when javascript files are changed.
|
75
|
+
def cache_sweeper(watches: nil)
|
76
|
+
if watches
|
77
|
+
@cache_sweeper =
|
78
|
+
Importmap.framework.application.config.file_watcher.new([], Array(watches).collect { |dir| [ dir.to_s, "js"] }.to_h) do
|
79
|
+
clear_cache
|
80
|
+
end
|
81
|
+
else
|
82
|
+
@cache_sweeper
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
MappedDir = Struct.new(:dir, :path, :under, :preload, keyword_init: true)
|
88
|
+
MappedFile = Struct.new(:name, :path, :preload, keyword_init: true)
|
89
|
+
|
90
|
+
def cache_as(name)
|
91
|
+
if result = @cache[name.to_s]
|
92
|
+
result
|
93
|
+
else
|
94
|
+
@cache[name.to_s] = yield
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def clear_cache
|
99
|
+
@cache.clear
|
100
|
+
end
|
101
|
+
|
102
|
+
def rescuable_asset_error?(error)
|
103
|
+
Importmap.framework.application.config.importmap.rescuable_asset_errors.any? { |e| error.is_a?(e) }
|
104
|
+
end
|
105
|
+
|
106
|
+
def resolve_asset_paths(paths, resolver:)
|
107
|
+
paths.transform_values do |mapping|
|
108
|
+
begin
|
109
|
+
resolver.path_to_asset(mapping.path)
|
110
|
+
rescue => e
|
111
|
+
if rescuable_asset_error?(e)
|
112
|
+
Importmap.framework.logger.warn "Importmap skipped missing path: #{mapping.path}"
|
113
|
+
nil
|
114
|
+
else
|
115
|
+
raise e
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end.compact
|
119
|
+
end
|
120
|
+
|
121
|
+
def expanded_preloading_packages_and_directories
|
122
|
+
expanded_packages_and_directories.select { |name, mapping| mapping.preload }
|
123
|
+
end
|
124
|
+
|
125
|
+
def expanded_packages_and_directories
|
126
|
+
@packages.dup.tap { |expanded| expand_directories_into expanded }
|
127
|
+
end
|
128
|
+
|
129
|
+
def expand_directories_into(paths)
|
130
|
+
@directories.values.each do |mapping|
|
131
|
+
if (absolute_path = absolute_root_of(mapping.dir)).exist?
|
132
|
+
find_javascript_files_in_tree(absolute_path).each do |filename|
|
133
|
+
module_filename = filename.relative_path_from(absolute_path)
|
134
|
+
module_name = module_name_from(module_filename, mapping)
|
135
|
+
module_path = module_path_from(module_filename, mapping)
|
136
|
+
|
137
|
+
paths[module_name] = MappedFile.new(name: module_name, path: module_path, preload: mapping.preload)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def module_name_from(filename, mapping)
|
144
|
+
[ mapping.under, filename.to_s.remove(filename.extname).remove(/\/?index$/).presence ].compact.join("/")
|
145
|
+
end
|
146
|
+
|
147
|
+
def module_path_from(filename, mapping)
|
148
|
+
[ mapping.path || mapping.under, filename.to_s ].compact.join("/")
|
149
|
+
end
|
150
|
+
|
151
|
+
def find_javascript_files_in_tree(path)
|
152
|
+
Dir[path.join("**/*.js{,m}")].collect { |file| Pathname.new(file) }.select(&:file?)
|
153
|
+
end
|
154
|
+
|
155
|
+
def absolute_root_of(path)
|
156
|
+
(pathname = Pathname.new(path)).absolute? ? pathname : Importmap.framework.root.join(path)
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "uri"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Importmap
|
6
|
+
class Npm
|
7
|
+
Error = Class.new(StandardError)
|
8
|
+
HTTPError = Class.new(Error)
|
9
|
+
|
10
|
+
singleton_class.attr_accessor :base_uri
|
11
|
+
self.base_uri = URI("https://registry.npmjs.org")
|
12
|
+
|
13
|
+
def initialize(importmap_path = "config/importmap.rb")
|
14
|
+
@importmap_path = Pathname.new(importmap_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def outdated_packages
|
18
|
+
packages_with_versions.each.with_object([]) do |(package, current_version), outdated_packages|
|
19
|
+
outdated_package = OutdatedPackage.new(name: package,
|
20
|
+
current_version: current_version)
|
21
|
+
|
22
|
+
if !(response = get_package(package))
|
23
|
+
outdated_package.error = 'Response error'
|
24
|
+
elsif (error = response['error'])
|
25
|
+
outdated_package.error = error
|
26
|
+
else
|
27
|
+
latest_version = find_latest_version(response)
|
28
|
+
next unless outdated?(current_version, latest_version)
|
29
|
+
|
30
|
+
outdated_package.latest_version = latest_version
|
31
|
+
end
|
32
|
+
|
33
|
+
outdated_packages << outdated_package
|
34
|
+
end.sort_by(&:name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def vulnerable_packages
|
38
|
+
get_audit.flat_map do |package, vulnerabilities|
|
39
|
+
vulnerabilities.map do |vulnerability|
|
40
|
+
VulnerablePackage.new(name: package,
|
41
|
+
severity: vulnerability['severity'],
|
42
|
+
vulnerable_versions: vulnerability['vulnerable_versions'],
|
43
|
+
vulnerability: vulnerability['title'])
|
44
|
+
end
|
45
|
+
end.sort_by { |p| [p.name, p.severity] }
|
46
|
+
end
|
47
|
+
|
48
|
+
def packages_with_versions
|
49
|
+
# We cannot use the name after "pin" because some dependencies are loaded from inside packages
|
50
|
+
# Eg. pin "buffer", to: "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.19/nodelibs/browser/buffer.js"
|
51
|
+
|
52
|
+
importmap.scan(/^pin .*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/)(.*)(?=@\d+\.\d+\.\d+)@(\d+\.\d+\.\d+(?:[^\/\s"]*)).*$/) |
|
53
|
+
importmap.scan(/^pin "([^"]*)".* #.*@(\d+\.\d+\.\d+(?:[^\s]*)).*$/)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
OutdatedPackage = Struct.new(:name, :current_version, :latest_version, :error, keyword_init: true)
|
58
|
+
VulnerablePackage = Struct.new(:name, :severity, :vulnerable_versions, :vulnerability, keyword_init: true)
|
59
|
+
|
60
|
+
|
61
|
+
|
62
|
+
def importmap
|
63
|
+
@importmap ||= File.read(@importmap_path)
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_package(package)
|
67
|
+
uri = self.class.base_uri.dup
|
68
|
+
uri.path = "/" + package
|
69
|
+
response = get_json(uri)
|
70
|
+
|
71
|
+
JSON.parse(response)
|
72
|
+
rescue JSON::ParserError
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_json(uri)
|
77
|
+
request = Net::HTTP::Get.new(uri)
|
78
|
+
request["Content-Type"] = "application/json"
|
79
|
+
|
80
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http|
|
81
|
+
http.request(request)
|
82
|
+
}
|
83
|
+
|
84
|
+
response.body
|
85
|
+
rescue => error
|
86
|
+
raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})"
|
87
|
+
end
|
88
|
+
|
89
|
+
def find_latest_version(response)
|
90
|
+
latest_version = response.dig('dist-tags', 'latest')
|
91
|
+
return latest_version if latest_version
|
92
|
+
|
93
|
+
return unless response['versions']
|
94
|
+
|
95
|
+
response['versions'].keys.map { |v| Gem::Version.new(v) rescue nil }.compact.sort.last
|
96
|
+
end
|
97
|
+
|
98
|
+
def outdated?(current_version, latest_version)
|
99
|
+
Gem::Version.new(current_version) < Gem::Version.new(latest_version)
|
100
|
+
rescue ArgumentError
|
101
|
+
current_version.to_s < latest_version.to_s
|
102
|
+
end
|
103
|
+
|
104
|
+
def get_audit
|
105
|
+
uri = self.class.base_uri.dup
|
106
|
+
uri.path = "/-/npm/v1/security/advisories/bulk"
|
107
|
+
|
108
|
+
body = packages_with_versions.each.with_object({}) { |(package, version), data|
|
109
|
+
data[package] ||= []
|
110
|
+
data[package] << version
|
111
|
+
}
|
112
|
+
return {} if body.empty?
|
113
|
+
|
114
|
+
response = post_json(uri, body)
|
115
|
+
JSON.parse(response.body)
|
116
|
+
end
|
117
|
+
|
118
|
+
def post_json(uri, body)
|
119
|
+
Net::HTTP.post(uri, body.to_json, "Content-Type" => "application/json")
|
120
|
+
rescue => error
|
121
|
+
raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "uri"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Importmap
|
6
|
+
class Packager
|
7
|
+
Error = Class.new(StandardError)
|
8
|
+
HTTPError = Class.new(Error)
|
9
|
+
ServiceError = Error.new(Error)
|
10
|
+
|
11
|
+
singleton_class.attr_accessor :endpoint
|
12
|
+
self.endpoint = URI("https://api.jspm.io/generate")
|
13
|
+
|
14
|
+
attr_reader :vendor_path
|
15
|
+
|
16
|
+
def initialize(importmap_path = "config/importmap.rb", vendor_path: "vendor/javascript")
|
17
|
+
@importmap_path = Pathname.new(importmap_path)
|
18
|
+
@vendor_path = Pathname.new(vendor_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def import(*packages, env: "production", from: "jspm")
|
22
|
+
response = post_json({
|
23
|
+
"install" => Array(packages),
|
24
|
+
"flattenScope" => true,
|
25
|
+
"env" => [ "browser", "module", env ],
|
26
|
+
"provider" => from.to_s,
|
27
|
+
})
|
28
|
+
|
29
|
+
case response.code
|
30
|
+
when "200" then extract_parsed_imports(response)
|
31
|
+
when "404", "401" then nil
|
32
|
+
else handle_failure_response(response)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def pin_for(package, url)
|
37
|
+
%(pin "#{package}", to: "#{url}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def vendored_pin_for(package, url)
|
41
|
+
filename = package_filename(package)
|
42
|
+
version = extract_package_version_from(url)
|
43
|
+
|
44
|
+
if "#{package}.js" == filename
|
45
|
+
%(pin "#{package}" # #{version})
|
46
|
+
else
|
47
|
+
%(pin "#{package}", to: "#{filename}" # #{version})
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def packaged?(package)
|
52
|
+
importmap.match(/^pin ["']#{package}["'].*$/)
|
53
|
+
end
|
54
|
+
|
55
|
+
def download(package, url)
|
56
|
+
ensure_vendor_directory_exists
|
57
|
+
remove_existing_package_file(package)
|
58
|
+
download_package_file(package, url)
|
59
|
+
end
|
60
|
+
|
61
|
+
def remove(package)
|
62
|
+
remove_existing_package_file(package)
|
63
|
+
remove_package_from_importmap(package)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def post_json(body)
|
68
|
+
Net::HTTP.post(self.class.endpoint, body.to_json, "Content-Type" => "application/json")
|
69
|
+
rescue => error
|
70
|
+
raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})"
|
71
|
+
end
|
72
|
+
|
73
|
+
def extract_parsed_imports(response)
|
74
|
+
JSON.parse(response.body).dig("map", "imports")
|
75
|
+
end
|
76
|
+
|
77
|
+
def handle_failure_response(response)
|
78
|
+
if error_message = parse_service_error(response)
|
79
|
+
raise ServiceError, error_message
|
80
|
+
else
|
81
|
+
raise HTTPError, "Unexpected response code (#{response.code})"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse_service_error(response)
|
86
|
+
JSON.parse(response.body.to_s)["error"]
|
87
|
+
rescue JSON::ParserError
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
|
91
|
+
def importmap
|
92
|
+
@importmap ||= File.read(@importmap_path)
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def ensure_vendor_directory_exists
|
97
|
+
FileUtils.mkdir_p @vendor_path
|
98
|
+
end
|
99
|
+
|
100
|
+
def remove_existing_package_file(package)
|
101
|
+
FileUtils.rm_rf vendored_package_path(package)
|
102
|
+
end
|
103
|
+
|
104
|
+
def remove_package_from_importmap(package)
|
105
|
+
all_lines = File.readlines(@importmap_path)
|
106
|
+
with_lines_removed = all_lines.grep_v(/pin ["']#{package}["']/)
|
107
|
+
|
108
|
+
File.open(@importmap_path, "w") do |file|
|
109
|
+
with_lines_removed.each { |line| file.write(line) }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def download_package_file(package, url)
|
114
|
+
response = Net::HTTP.get_response(URI(url))
|
115
|
+
|
116
|
+
if response.code == "200"
|
117
|
+
save_vendored_package(package, response.body)
|
118
|
+
else
|
119
|
+
handle_failure_response(response)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def save_vendored_package(package, source)
|
124
|
+
File.open(vendored_package_path(package), "w+") do |vendored_package|
|
125
|
+
vendored_package.write remove_sourcemap_comment_from(source).force_encoding("UTF-8")
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def remove_sourcemap_comment_from(source)
|
130
|
+
source.gsub(/^\/\/# sourceMappingURL=.*/, "")
|
131
|
+
end
|
132
|
+
|
133
|
+
def vendored_package_path(package)
|
134
|
+
@vendor_path.join(package_filename(package))
|
135
|
+
end
|
136
|
+
|
137
|
+
def package_filename(package)
|
138
|
+
package.gsub("/", "--") + ".js"
|
139
|
+
end
|
140
|
+
|
141
|
+
def extract_package_version_from(url)
|
142
|
+
url.match(/@\d+\.\d+\.\d+/)&.to_a&.first
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Importmap::Reloader
|
2
|
+
delegate :execute_if_updated, :execute, :updated?, to: :updater
|
3
|
+
|
4
|
+
def reload!
|
5
|
+
import_map_paths.each { |path| Importmap.framework.application.importmap.draw(path) }
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
def updater
|
10
|
+
@updater ||= config.file_watcher.new(import_map_paths) { reload! }
|
11
|
+
end
|
12
|
+
|
13
|
+
def import_map_paths
|
14
|
+
config.importmap.paths
|
15
|
+
end
|
16
|
+
|
17
|
+
def config
|
18
|
+
Importmap.framework.application.config
|
19
|
+
end
|
20
|
+
end
|
data/lib/importmap.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
$stdout.sync = true unless ENV["IMPORTMAP_STDOUT_SYNC"] == "0"
|
2
|
+
|
3
|
+
$:.unshift(File.expand_path("../", __FILE__))
|
4
|
+
|
5
|
+
require "importmap/autoloader"
|
6
|
+
Importmap::Autoloader.setup
|
7
|
+
|
8
|
+
require "memoist"
|
9
|
+
require "pathname"
|
10
|
+
require "rainbow/ext/string"
|
11
|
+
|
12
|
+
module Importmap
|
13
|
+
class Error < StandardError; end
|
14
|
+
|
15
|
+
def framework
|
16
|
+
Framework.instance.framework_class
|
17
|
+
end
|
18
|
+
|
19
|
+
def framework_boot
|
20
|
+
Framework.instance.boot
|
21
|
+
end
|
22
|
+
|
23
|
+
extend self
|
24
|
+
end
|
data/spec/cli_spec.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
describe Importmap::CLI do
|
2
|
+
before(:all) do
|
3
|
+
@args = "--from Tung"
|
4
|
+
end
|
5
|
+
|
6
|
+
describe "importmap" do
|
7
|
+
it "hello" do
|
8
|
+
out = execute("exe/importmap hello world #{@args}")
|
9
|
+
expect(out).to include("from: Tung\nHello world")
|
10
|
+
end
|
11
|
+
|
12
|
+
commands = {
|
13
|
+
"hell" => "hello",
|
14
|
+
"hello" => "name",
|
15
|
+
"hello -" => "--from",
|
16
|
+
"hello name" => "--from",
|
17
|
+
"hello name --" => "--from",
|
18
|
+
}
|
19
|
+
commands.each do |command, expected_word|
|
20
|
+
it "completion #{command}" do
|
21
|
+
out = execute("exe/importmap completion #{command}")
|
22
|
+
expect(out).to include(expected_word) # only checking for one word for simplicity
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
ENV["IMPORTMAP_TEST"] = "1"
|
2
|
+
|
3
|
+
# CodeClimate test coverage: https://docs.codeclimate.com/docs/configuring-test-coverage
|
4
|
+
# require 'simplecov'
|
5
|
+
# SimpleCov.start
|
6
|
+
|
7
|
+
require "pp"
|
8
|
+
require "byebug"
|
9
|
+
root = File.expand_path("../", File.dirname(__FILE__))
|
10
|
+
require "#{root}/lib/importmap"
|
11
|
+
|
12
|
+
module Helper
|
13
|
+
def execute(cmd)
|
14
|
+
puts "Running: #{cmd}" if show_command?
|
15
|
+
out = `#{cmd}`
|
16
|
+
puts out if show_command?
|
17
|
+
out
|
18
|
+
end
|
19
|
+
|
20
|
+
# Added SHOW_COMMAND because DEBUG is also used by other libraries like
|
21
|
+
# bundler and it shows its internal debugging logging also.
|
22
|
+
def show_command?
|
23
|
+
ENV['DEBUG'] || ENV['SHOW_COMMAND']
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
RSpec.configure do |c|
|
28
|
+
c.include Helper
|
29
|
+
end
|