import_js 0.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 330ead6ec7804b53e85225024d9d66d001c29719
4
+ data.tar.gz: d959cff0e6e106e9035276a1ad69ecb740abbe74
5
+ SHA512:
6
+ metadata.gz: c6c7c887852626478ab3a3496082ec2733e03570fe46fb3aef9a113a7d490645d9c3485913c611c424e3b51cc33831c148598668b912cc51d78aa6a76579a70c
7
+ data.tar.gz: eee49961904abde1dbc4135263ed7c637ae8920a76060137c9ed1f679d393f33c4f620c8d6605ed297e16208d42cf2347c927a0c68e69f27f878490b28cec058
@@ -0,0 +1,128 @@
1
+ require 'json'
2
+ require 'open3'
3
+
4
+ module ImportJS
5
+ CONFIG_FILE = '.importjs.json'
6
+
7
+ DEFAULT_CONFIG = {
8
+ 'aliases' => {},
9
+ 'declaration_keyword' => 'var',
10
+ 'excludes' => [],
11
+ 'jshint_cmd' => 'jshint',
12
+ 'lookup_paths' => ['.'],
13
+ }
14
+
15
+ # Class that initializes configuration from a .importjs.json file
16
+ class Configuration
17
+ def initialize
18
+ @config = DEFAULT_CONFIG.merge(load_config)
19
+ end
20
+
21
+ def refresh
22
+ return if @config_time == config_file_last_modified
23
+ @config = DEFAULT_CONFIG.merge(load_config)
24
+ end
25
+
26
+ # @return [Object] a configuration value
27
+ def get(key)
28
+ @config[key]
29
+ end
30
+
31
+ def resolve_alias(variable_name)
32
+ path = @config['aliases'][variable_name]
33
+ return resolve_destructured_alias(variable_name) unless path
34
+
35
+ path = path['path'] if path.is_a? Hash
36
+ ImportJS::JSModule.new(nil, path, self)
37
+ end
38
+
39
+ def resolve_destructured_alias(variable_name)
40
+ @config['aliases'].each do |_, path|
41
+ next if path.is_a? String
42
+ if (path['destructure'] || []).include?(variable_name)
43
+ js_module = ImportJS::JSModule.new(nil, path['path'], self)
44
+ js_module.is_destructured = true
45
+ return js_module
46
+ end
47
+ end
48
+ nil
49
+ end
50
+
51
+ # @return [Number?]
52
+ def columns
53
+ get_number('&columns')
54
+ end
55
+
56
+ # @return [Number?]
57
+ def text_width
58
+ get_number('&textwidth')
59
+ end
60
+
61
+ # @return [String] shiftwidth number of spaces if expandtab is not set,
62
+ # otherwise `\t`
63
+ def tab
64
+ return "\t" unless expand_tab?
65
+ ' ' * (shift_width || 2)
66
+ end
67
+
68
+ # @return [Array<String>]
69
+ def package_dependencies
70
+ return [] unless File.exist?('package.json')
71
+
72
+ package = JSON.parse(File.read('package.json'))
73
+ dependencies = package['dependencies'] ?
74
+ package['dependencies'].keys : []
75
+ peer_dependencies = package['peerDependencies'] ?
76
+ package['peerDependencies'].keys : []
77
+
78
+ dependencies.concat(peer_dependencies)
79
+ end
80
+
81
+ private
82
+
83
+ # @return [Hash]
84
+ def load_config
85
+ @config_time = config_file_last_modified
86
+ File.exist?(CONFIG_FILE) ? JSON.parse(File.read(CONFIG_FILE)) : {}
87
+ end
88
+
89
+ # @return [Time?]
90
+ def config_file_last_modified
91
+ File.exist?(CONFIG_FILE) ? File.mtime(CONFIG_FILE) : nil
92
+ end
93
+
94
+ # Check for the presence of a setting such as:
95
+ #
96
+ # - g:CommandTSmartCase (plug-in setting)
97
+ # - &wildignore (Vim setting)
98
+ # - +cursorcolumn (Vim setting, that works)
99
+ #
100
+ # @param str [String]
101
+ # @return [Boolean]
102
+ def exists?(str)
103
+ VIM.evaluate(%{exists("#{str}")}).to_i != 0
104
+ end
105
+
106
+ # @param name [String]
107
+ # @return [Number?]
108
+ def get_number(name)
109
+ exists?(name) ? VIM.evaluate("#{name}").to_i : nil
110
+ end
111
+
112
+ # @param name [String]
113
+ # @return [Boolean?]
114
+ def get_bool(name)
115
+ exists?(name) ? VIM.evaluate("#{name}").to_i != 0 : nil
116
+ end
117
+
118
+ # @return [Boolean?]
119
+ def expand_tab?
120
+ get_bool('&expandtab')
121
+ end
122
+
123
+ # @return [Number?]
124
+ def shift_width
125
+ get_number('&shiftwidth')
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,288 @@
1
+ # encoding: utf-8
2
+ require 'json'
3
+ require 'open3'
4
+
5
+ module ImportJS
6
+ class Importer
7
+ def initialize(editor = ImportJS::VIMEditor.new)
8
+ @config = ImportJS::Configuration.new
9
+ @editor = editor
10
+ end
11
+
12
+ # Finds variable under the cursor to import. By default, this is bound to
13
+ # `<Leader>j`.
14
+ def import
15
+ @config.refresh
16
+ variable_name = @editor.current_word
17
+ if variable_name.empty?
18
+ message(<<-EOS.split.join(' '))
19
+ No variable to import. Place your cursor on a variable, then try
20
+ again.
21
+ EOS
22
+ return
23
+ end
24
+ current_row, current_col = @editor.cursor
25
+
26
+ old_buffer_lines = @editor.count_lines
27
+ import_one_variable variable_name
28
+ return unless lines_changed = @editor.count_lines - old_buffer_lines
29
+ @editor.cursor = [current_row + lines_changed, current_col]
30
+ end
31
+
32
+ def goto
33
+ @config.refresh
34
+ @timing = { start: Time.now }
35
+ variable_name = @editor.current_word
36
+ js_modules = find_js_modules(variable_name)
37
+ @timing[:end] = Time.now
38
+ return if js_modules.empty?
39
+ js_module = resolve_one_js_module(js_modules, variable_name)
40
+ @editor.open_file(js_module.file_path)
41
+ end
42
+
43
+ # Finds all variables that haven't yet been imported.
44
+ def import_all
45
+ @config.refresh
46
+ unused_variables = find_unused_variables
47
+
48
+ if unused_variables.empty?
49
+ message('No variables to import')
50
+ return
51
+ end
52
+
53
+ unused_variables.each do |variable|
54
+ import_one_variable(variable)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def message(str)
61
+ str = "[import-js] #{str}"
62
+ if str.length > @config.columns - 1
63
+ str = str[0...(@config.columns - 2)] + '…'
64
+ end
65
+
66
+ @editor.message(str)
67
+ end
68
+
69
+ # @return [Array]
70
+ def find_unused_variables
71
+ content = "/* jshint undef: true, strict: true */\n" +
72
+ "/* eslint no-unused-vars: [2, { \"vars\": \"all\", \"args\": \"none\" }] */\n" +
73
+ @editor.current_file_content
74
+
75
+ out, _ = Open3.capture3("#{@config.get('jshint_cmd')} -", stdin_data: content)
76
+ result = []
77
+ out.split("\n").each do |line|
78
+ /.*['"]([^'"]+)['"] is not defined/.match(line) do |match_data|
79
+ result << match_data[1]
80
+ end
81
+ end
82
+ result.uniq
83
+ end
84
+
85
+ # @param variable_name [String]
86
+ def import_one_variable(variable_name)
87
+ @timing = { start: Time.now }
88
+ js_modules = find_js_modules(variable_name)
89
+ @timing[:end] = Time.now
90
+ if js_modules.empty?
91
+ message(<<-EOS.split.join(' '))
92
+ No js module to import for variable `#{variable_name}` #{timing}
93
+ EOS
94
+ return
95
+ end
96
+
97
+ resolved_js_module = resolve_one_js_module(js_modules, variable_name)
98
+ return unless resolved_js_module
99
+
100
+ write_imports(variable_name, resolved_js_module)
101
+ end
102
+
103
+ # @param variable_name [String]
104
+ # @param js_module [ImportJS::JSModule]
105
+ def write_imports(variable_name, js_module)
106
+ old_imports = find_current_imports
107
+
108
+ # Ensure that there is a blank line after the block of all imports
109
+ unless @editor.read_line(old_imports[:newline_count] + 1).strip.empty?
110
+ @editor.append_line(old_imports[:newline_count], '')
111
+ end
112
+
113
+ modified_imports = old_imports[:imports] # Array
114
+
115
+ # Add new import to the block of imports, wrapping at text_width
116
+ unless js_module.is_destructured && inject_destructured_variable(
117
+ variable_name, js_module, modified_imports)
118
+ modified_imports << generate_import(variable_name, js_module)
119
+ end
120
+
121
+ # Sort the block of imports
122
+ modified_imports.sort!.uniq! do |import|
123
+ # Determine uniqueness by discarding the declaration keyword (`const`,
124
+ # `let`, or `var`) and normalizing multiple whitespace chars to single
125
+ # spaces.
126
+ import.sub(/\A(const|let|var)\s+/, '').sub(/\s\s+/s, ' ')
127
+ end
128
+
129
+ # Delete old imports, then add the modified list back in.
130
+ old_imports[:newline_count].times { @editor.delete_line(1) }
131
+ modified_imports.reverse_each do |import|
132
+ # We need to add each line individually because the Vim buffer will
133
+ # convert newline characters to `~@`.
134
+ import.split("\n").reverse_each { |line| @editor.append_line(0, line) }
135
+ end
136
+ end
137
+
138
+ def inject_destructured_variable(variable_name, js_module, imports)
139
+ imports.each do |import|
140
+ match = import.match(%r{((const|let|var) \{ )(.*)( \} = require\('#{js_module.import_path}'\);)})
141
+ next unless match
142
+
143
+ variables = match[3].split(/,\s*/).concat([variable_name]).uniq.sort
144
+ import.sub!(/.*/, "#{match[1]}#{variables.join(', ')}#{match[4]}")
145
+ return true
146
+ end
147
+ false
148
+ end
149
+
150
+ # @return [Hash]
151
+ def find_current_imports
152
+ potential_import_lines = []
153
+ @editor.count_lines.times do |n|
154
+ line = @editor.read_line(n + 1)
155
+ break if line.strip.empty?
156
+ potential_import_lines << line
157
+ end
158
+
159
+ # We need to put the potential imports back into a blob in order to scan
160
+ # for multiline imports
161
+ potential_imports_blob = potential_import_lines.join("\n")
162
+
163
+ imports = []
164
+
165
+ # Scan potential imports for everything ending in a semicolon, then
166
+ # iterate through those and stop at anything that's not an import.
167
+ potential_imports_blob.scan(/^.*?;/m).each do |potential_import|
168
+ break unless potential_import.match(
169
+ /(?:const|let|var)\s+.+=\s+require\(.*\).*;/)
170
+ imports << potential_import
171
+ end
172
+
173
+ newline_count = imports.length + imports.reduce(0) do |sum, import|
174
+ sum + import.scan(/\n/).length
175
+ end
176
+ {
177
+ imports: imports,
178
+ newline_count: newline_count
179
+ }
180
+ end
181
+
182
+ # @param variable_name [String]
183
+ # @param js_module [ImportJS::JSModule]
184
+ # @return [String] the import string to be added to the imports block
185
+ def generate_import(variable_name, js_module)
186
+ declaration_keyword = @config.get('declaration_keyword')
187
+ if js_module.is_destructured
188
+ declaration = "#{declaration_keyword} { #{variable_name} } ="
189
+ else
190
+ declaration = "#{declaration_keyword} #{variable_name} ="
191
+ end
192
+ value = "require('#{js_module.import_path}');"
193
+
194
+ if @config.text_width && "#{declaration} #{value}".length > @config.text_width
195
+ "#{declaration}\n#{@config.tab}#{value}"
196
+ else
197
+ "#{declaration} #{value}"
198
+ end
199
+ end
200
+
201
+ # @param variable_name [String]
202
+ # @return [Array]
203
+ def find_js_modules(variable_name)
204
+ if alias_module = @config.resolve_alias(variable_name)
205
+ return [alias_module]
206
+ end
207
+
208
+ egrep_command =
209
+ "egrep -i \"(/|^)#{formatted_to_regex(variable_name)}(/index)?(/package)?\.js.*\""
210
+ matched_modules = []
211
+ @config.get('lookup_paths').each do |lookup_path|
212
+ find_command = "find #{lookup_path} -name \"**.js*\""
213
+ out, _ = Open3.capture3("#{find_command} | #{egrep_command}")
214
+ matched_modules.concat(
215
+ out.split("\n").map do |f|
216
+ next if @config.get('excludes').any? do |glob_pattern|
217
+ File.fnmatch(glob_pattern, f)
218
+ end
219
+ js_module = ImportJS::JSModule.new(lookup_path, f, @config)
220
+ next if js_module.skip
221
+ js_module
222
+ end.compact
223
+ )
224
+ end
225
+
226
+ # Find imports from package.json
227
+ @config.package_dependencies.each do |dep|
228
+ next unless dep =~ /^#{formatted_to_regex(variable_name)}$/
229
+ js_module = ImportJS::JSModule.new(
230
+ 'node_modules', "node_modules/#{dep}/package.json", @config)
231
+ next if js_module.skip
232
+ matched_modules << js_module
233
+ end
234
+
235
+ # If you have overlapping lookup paths, you might end up seeing the same
236
+ # module to import twice. In order to dedupe these, we remove the module
237
+ # with the longest path
238
+ matched_modules.sort do |a, b|
239
+ a.import_path.length <=> b.import_path.length
240
+ end.uniq do |m|
241
+ m.lookup_path + '/' + m.import_path
242
+ end.sort do |a, b|
243
+ a.display_name <=> b.display_name
244
+ end
245
+ end
246
+
247
+ # @param js_modules [Array]
248
+ # @param variable_name [String]
249
+ # @return [String]
250
+ def resolve_one_js_module(js_modules, variable_name)
251
+ if js_modules.length == 1
252
+ message("Imported `#{js_modules.first.display_name}` #{timing}")
253
+ return js_modules.first
254
+ end
255
+
256
+ selected_index = @editor.ask_for_selection(
257
+ "\"[import-js] Pick js module to import for '#{variable_name}': #{timing}\"",
258
+ js_modules.map {|m| m.display_name}
259
+ )
260
+ return unless selected_index
261
+ js_modules[selected_index]
262
+ end
263
+
264
+ # Takes a string in any of the following four formats:
265
+ # dash-separated
266
+ # snake_case
267
+ # camelCase
268
+ # PascalCase
269
+ # and turns it into a star-separated lower case format, like so:
270
+ # star*separated
271
+ #
272
+ # @param string [String]
273
+ # @return [String]
274
+ def formatted_to_regex(string)
275
+ # Based on
276
+ # http://stackoverflow.com/questions/1509915/converting-camel-case-to-underscore-case-in-ruby
277
+ string.
278
+ gsub(/([a-z\d])([A-Z])/, '\1.?\2'). # separates camelCase words with '.?'
279
+ tr('-_', '.'). # replaces underscores or dashes with '.'
280
+ downcase # converts all upper to lower case
281
+ end
282
+
283
+ # @return [String]
284
+ def timing
285
+ "(#{(@timing[:end] - @timing[:start]).round(2)}s)"
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,46 @@
1
+ module ImportJS
2
+ # Class that represents a js module found in the file system
3
+ class JSModule
4
+ attr_reader :import_path
5
+ attr_reader :lookup_path
6
+ attr_reader :file_path
7
+ attr_reader :main_file
8
+ attr_reader :skip
9
+ attr_accessor :is_destructured
10
+
11
+ # @param lookup_path [String] the lookup path in which this module was found
12
+ # @param relative_file_path [String] a full path to the file, relative to
13
+ # the project root.
14
+ # @param configuration [ImportJS::Configuration]
15
+ def initialize(lookup_path, relative_file_path, configuration)
16
+ @lookup_path = lookup_path
17
+ @file_path = relative_file_path
18
+ if relative_file_path.end_with? '/package.json'
19
+ @main_file = JSON.parse(File.read(relative_file_path))['main']
20
+ match = relative_file_path.match(/(.*)\/package\.json/)
21
+ @import_path = match[1]
22
+ @skip = !@main_file
23
+ elsif relative_file_path.match(/\/index\.js.*$/)
24
+ match = relative_file_path.match(/(.*)\/(index\.js.*)/)
25
+ @main_file = match[2]
26
+ @import_path = match[1]
27
+ else
28
+ @import_path = relative_file_path
29
+ unless configuration.get('keep_file_extensions')
30
+ @import_path = @import_path.sub(/\.js.*$/, '')
31
+ end
32
+ end
33
+
34
+ if lookup_path
35
+ @import_path = @import_path.sub("#{@lookup_path}\/", '') # remove path prefix
36
+ end
37
+ end
38
+
39
+ # @return [String] a readable description of the module
40
+ def display_name
41
+ parts = [import_path]
42
+ parts << " (main: #{@main_file})" if @main_file
43
+ parts.join('')
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,98 @@
1
+ module ImportJS
2
+ # This is the implementation of the VIM integration in Import-JS. It can be
3
+ # used as a template for other editor integrations.
4
+ class VIMEditor
5
+ # Get the word currently under the cursor.
6
+ #
7
+ # @return [String]
8
+ def current_word
9
+ VIM.evaluate("expand('<cword>')")
10
+ end
11
+
12
+ # Open a file specified by a path.
13
+ #
14
+ # @param file_path [String]
15
+ def open_file(file_path)
16
+ VIM.command("e #{file_path}")
17
+ end
18
+
19
+ # Display a message to the user.
20
+ #
21
+ # @param str [String]
22
+ def message(str)
23
+ VIM.command(":call importjs#WideMsg('#{str}')")
24
+ end
25
+
26
+ # Read the entire file into a string.
27
+ #
28
+ # @return [String]
29
+ def current_file_content
30
+ VIM.evaluate('join(getline(1, "$"), "\n")')
31
+ end
32
+
33
+ # Reads a line from the file.
34
+ #
35
+ # Lines are one-indexed, so 1 means the first line in the file.
36
+ # @return [String]
37
+ def read_line(line_number)
38
+ VIM::Buffer.current[line_number]
39
+ end
40
+
41
+ # Get the cursor position.
42
+ #
43
+ # @return [Array(Number, Number)]
44
+ def cursor
45
+ VIM::Window.current.cursor
46
+ end
47
+
48
+ # Place the cursor at a specified position.
49
+ #
50
+ # @param position_tuple [Array(Number, Number)] the row and column to place
51
+ # the cursor at.
52
+ def cursor=(position_tuple)
53
+ VIM::Window.current.cursor = position_tuple
54
+ end
55
+
56
+ # Delete a line.
57
+ #
58
+ # @param line_number [Number] One-indexed line number.
59
+ # 1 is the first line in the file.
60
+ def delete_line(line_number)
61
+ VIM::Buffer.current.delete(line_number)
62
+ end
63
+
64
+ # Append a line right after the specified line.
65
+ #
66
+ # Lines are one-indexed, but you need to support appending to line 0 (add
67
+ # content at top of file).
68
+ # @param line_number [Number]
69
+ def append_line(line_number, str)
70
+ VIM::Buffer.current.append(line_number, str)
71
+ end
72
+
73
+ # Count the number of lines in the file.
74
+ #
75
+ # @return [Number] the number of lines in the file
76
+ def count_lines
77
+ VIM::Buffer.current.count
78
+ end
79
+
80
+ # Ask the user to select something from a list of alternatives.
81
+ #
82
+ # @param heading [String] A heading text
83
+ # @param alternatives [Array<String>] A list of alternatives
84
+ # @return [Number, nil] the index of the selected alternative, or nil if
85
+ # nothing was selected.
86
+ def ask_for_selection(heading, alternatives)
87
+ escaped_list = [heading]
88
+ escaped_list << alternatives.each_with_index.map do |alternative, i|
89
+ "\"#{i + 1}: #{alternative}\""
90
+ end
91
+ escaped_list_string = '[' + escaped_list.join(',') + ']'
92
+
93
+ selected_index = VIM.evaluate("inputlist(#{escaped_list_string})")
94
+ return if selected_index < 1
95
+ selected_index - 1
96
+ end
97
+ end
98
+ end
data/lib/import_js.rb ADDED
@@ -0,0 +1,10 @@
1
+ # Namespace declaration
2
+ module ImportJS
3
+ # We initialize an empty "ImportJS" namespace here so that we can define
4
+ # classes under that namespace, e.g. `ImportJS::Importer`.
5
+ end
6
+
7
+ require_relative 'import_js/js_module'
8
+ require_relative 'import_js/importer'
9
+ require_relative 'import_js/vim_editor'
10
+ require_relative 'import_js/configuration'
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: import_js
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Henric Trotzig
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-11-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A tool to simplify importing javascript modules
14
+ email: henric.trotzig@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/import_js.rb
20
+ - lib/import_js/configuration.rb
21
+ - lib/import_js/importer.rb
22
+ - lib/import_js/js_module.rb
23
+ - lib/import_js/vim_editor.rb
24
+ homepage: http://rubygems.org/gems/import_js
25
+ licenses:
26
+ - MIT
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubyforge_project:
44
+ rubygems_version: 2.4.5.1
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: Import-JS
48
+ test_files: []
49
+ has_rdoc: