import_js 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: