import_js 0.3.1 → 0.4.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.
@@ -3,15 +3,20 @@ require 'open3'
3
3
 
4
4
  module ImportJS
5
5
  class Importer
6
+ REGEX_USE_STRICT = /(['"])use strict\1;?/
7
+ REGEX_SINGLE_LINE_COMMENT = %r{\A\s*//}
8
+ REGEX_MULTI_LINE_COMMENT_START = %r{\A\s*/\*}
9
+ REGEX_MULTI_LINE_COMMENT_END = %r{\*/}
10
+ REGEX_WHITESPACE_ONLY = /\A\s*\Z/
11
+
6
12
  def initialize(editor = ImportJS::VIMEditor.new)
7
- @config = ImportJS::Configuration.new
8
13
  @editor = editor
9
14
  end
10
15
 
11
16
  # Finds variable under the cursor to import. By default, this is bound to
12
17
  # `<Leader>j`.
13
18
  def import
14
- @config.refresh
19
+ @config = ImportJS::Configuration.new(@editor.path_to_current_file)
15
20
  variable_name = @editor.current_word
16
21
  if variable_name.empty?
17
22
  message(<<-EOS.split.join(' '))
@@ -20,48 +25,67 @@ module ImportJS
20
25
  EOS
21
26
  return
22
27
  end
23
- current_row, current_col = @editor.cursor
24
28
 
25
- old_buffer_lines = @editor.count_lines
26
29
  js_module = find_one_js_module(variable_name)
27
30
  return unless js_module
28
31
 
29
- old_imports = find_current_imports
30
- inject_js_module(variable_name, js_module, old_imports[:imports])
31
- replace_imports(old_imports[:newline_count],
32
- old_imports[:imports],
33
- old_imports[:imports_start_at])
34
- lines_changed = @editor.count_lines - old_buffer_lines
35
- return unless lines_changed
36
- @editor.cursor = [current_row + lines_changed, current_col]
32
+ maintain_cursor_position do
33
+ old_imports = find_current_imports
34
+ inject_js_module(variable_name, js_module, old_imports[:imports])
35
+ replace_imports(old_imports[:newline_count],
36
+ old_imports[:imports],
37
+ old_imports[:imports_start_at])
38
+ end
37
39
  end
38
40
 
39
41
  def goto
40
- @config.refresh
41
- @timing = { start: Time.now }
42
+ @config = ImportJS::Configuration.new(@editor.path_to_current_file)
43
+ js_modules = []
42
44
  variable_name = @editor.current_word
43
- js_modules = find_js_modules(variable_name)
44
- @timing[:end] = Time.now
45
+ time do
46
+ js_modules = find_js_modules(variable_name)
47
+ end
45
48
  return if js_modules.empty?
46
49
  js_module = resolve_one_js_module(js_modules, variable_name)
47
- @editor.open_file(js_module.file_path) if js_module
50
+ if js_module
51
+ @editor.open_file(js_module.open_file_path(@editor.path_to_current_file))
52
+ end
48
53
  end
49
54
 
55
+ REGEX_ESLINT_RESULT = /
56
+ (?<quote>["']) # <quote> opening quote
57
+ (?<variable_name>[^\1]+) # <variable_name>
58
+ \k<quote>
59
+ \s
60
+ (?<type> # <type>
61
+ is\sdefined\sbut\snever\sused # is defined but never used
62
+ |
63
+ is\snot\sdefined # is not defined
64
+ |
65
+ must\sbe\sin\sscope\swhen\susing\sJSX # must be in scope when using JSX
66
+ )
67
+ /x
68
+
50
69
  # Removes unused imports and adds imports for undefined variables
51
70
  def fix_imports
52
- @config.refresh
71
+ @config = ImportJS::Configuration.new(@editor.path_to_current_file)
53
72
  eslint_result = run_eslint_command
54
- undefined_variables = eslint_result.map do |line|
55
- /(["'])([^"']+)\1 is not defined/.match(line) do |match_data|
56
- match_data[2]
57
- end
58
- end.compact.uniq
59
73
 
60
- unused_variables = eslint_result.map do |line|
61
- /"([^"]+)" is defined but never used/.match(line) do |match_data|
62
- match_data[1]
74
+ unused_variables = []
75
+ undefined_variables = []
76
+
77
+ eslint_result.each do |line|
78
+ match = REGEX_ESLINT_RESULT.match(line)
79
+ next unless match
80
+ if match[:type] == 'is defined but never used'
81
+ unused_variables << match[:variable_name]
82
+ else
83
+ undefined_variables << match[:variable_name]
63
84
  end
64
- end.compact.uniq
85
+ end
86
+
87
+ unused_variables.uniq!
88
+ undefined_variables.uniq!
65
89
 
66
90
  old_imports = find_current_imports
67
91
  new_imports = old_imports[:imports].reject do |import_statement|
@@ -72,9 +96,8 @@ module ImportJS
72
96
  end
73
97
 
74
98
  undefined_variables.each do |variable|
75
- if js_module = find_one_js_module(variable)
76
- inject_js_module(variable, js_module, new_imports)
77
- end
99
+ js_module = find_one_js_module(variable)
100
+ inject_js_module(variable, js_module, new_imports) if js_module
78
101
  end
79
102
 
80
103
  replace_imports(old_imports[:newline_count],
@@ -88,10 +111,26 @@ module ImportJS
88
111
  @editor.message("ImportJS: #{str}")
89
112
  end
90
113
 
114
+ ESLINT_STDOUT_ERROR_REGEXES = [
115
+ /Parsing error: /,
116
+ /Unrecoverable syntax error/,
117
+ /<text>:0:0: Cannot find module '.*'/,
118
+ ].freeze
119
+
120
+ ESLINT_STDERR_ERROR_REGEXES = [
121
+ /SyntaxError: /,
122
+ /eslint: command not found/,
123
+ /Cannot read config package: /,
124
+ /Cannot find module '.*'/,
125
+ /No such file or directory/,
126
+ ].freeze
127
+
91
128
  # @return [Array<String>] the output from eslint, line by line
92
129
  def run_eslint_command
93
- command = @config.get('eslint_executable') + ' ' + %w[
130
+ command = %W[
131
+ #{@config.get('eslint_executable')}
94
132
  --stdin
133
+ --stdin-filename #{@editor.path_to_current_file}
95
134
  --format unix
96
135
  --rule 'no-undef: 2'
97
136
  --rule 'no-unused-vars: [2, { "vars": "all", "args": "none" }]'
@@ -99,17 +138,11 @@ module ImportJS
99
138
  out, err = Open3.capture3(command,
100
139
  stdin_data: @editor.current_file_content)
101
140
 
102
- if out =~ /Parsing error: / ||
103
- out =~ /Unrecoverable syntax error/ ||
104
- out =~ /<text>:0:0: Cannot find module '.*'/
141
+ if ESLINT_STDOUT_ERROR_REGEXES.any? { |regex| out =~ regex }
105
142
  fail ImportJS::ParseError.new, out
106
143
  end
107
144
 
108
- if err =~ /SyntaxError: / ||
109
- err =~ /eslint: command not found/ ||
110
- err =~ /Cannot read config package: / ||
111
- err =~ /Cannot find module '.*'/ ||
112
- err =~ /No such file or directory/
145
+ if ESLINT_STDERR_ERROR_REGEXES.any? { |regex| err =~ regex }
113
146
  fail ImportJS::ParseError.new, err
114
147
  end
115
148
 
@@ -119,9 +152,10 @@ module ImportJS
119
152
  # @param variable_name [String]
120
153
  # @return [ImportJS::JSModule?]
121
154
  def find_one_js_module(variable_name)
122
- @timing = { start: Time.now }
123
- js_modules = find_js_modules(variable_name)
124
- @timing[:end] = Time.now
155
+ js_modules = []
156
+ time do
157
+ js_modules = find_js_modules(variable_name)
158
+ end
125
159
  if js_modules.empty?
126
160
  message(
127
161
  "No JS module to import for variable `#{variable_name}` #{timing}")
@@ -136,45 +170,55 @@ module ImportJS
136
170
  # @param js_module [ImportJS::JSModule]
137
171
  # @param imports [Array<ImportJS::ImportStatement>]
138
172
  def inject_js_module(variable_name, js_module, imports)
139
- import = imports.find { |import| import.path == js_module.import_path }
173
+ import = imports.find do |an_import|
174
+ an_import.path == js_module.import_path
175
+ end
140
176
 
141
177
  if import
142
- if js_module.is_destructured
143
- import.inject_destructured_variable(variable_name)
178
+ import.declaration_keyword = @config.get(
179
+ 'declaration_keyword', from_file: js_module.file_path)
180
+ import.import_function = @config.get(
181
+ 'import_function', from_file: js_module.file_path)
182
+ if js_module.has_named_exports
183
+ import.inject_named_import(variable_name)
144
184
  else
145
- import.set_default_variable(variable_name)
185
+ import.set_default_import(variable_name)
146
186
  end
147
187
  else
148
- imports.unshift(js_module.to_import_statement(variable_name))
188
+ imports.unshift(js_module.to_import_statement(variable_name, @config))
149
189
  end
150
190
 
151
191
  # Remove duplicate import statements
152
192
  imports.uniq!(&:to_normalized)
153
193
  end
154
194
 
195
+ # @param imports [Array<ImportJS::ImportStatement>]
196
+ # @return [String]
197
+ def generate_import_strings(import_statements)
198
+ import_statements.map do |import|
199
+ import.to_import_strings(@editor.max_line_length, @editor.tab)
200
+ end.flatten.sort
201
+ end
202
+
155
203
  # @param old_imports_lines [Number]
156
204
  # @param new_imports [Array<ImportJS::ImportStatement>]
157
205
  # @param imports_start_at [Number]
158
206
  def replace_imports(old_imports_lines, new_imports, imports_start_at)
207
+ imports_end_at = old_imports_lines + imports_start_at
208
+
159
209
  # Ensure that there is a blank line after the block of all imports
160
210
  if old_imports_lines + new_imports.length > 0 &&
161
- !@editor.read_line(old_imports_lines + imports_start_at + 1).strip.empty?
162
- @editor.append_line(old_imports_lines + imports_start_at, '')
211
+ !@editor.read_line(imports_end_at + 1).strip.empty?
212
+ @editor.append_line(imports_end_at, '')
163
213
  end
164
214
 
165
- # Generate import strings
166
- import_strings = new_imports.map do |import|
167
- import.to_import_strings(
168
- @config.get('declaration_keyword'),
169
- @editor.max_line_length,
170
- @editor.tab)
171
- end.flatten.sort
215
+ import_strings = generate_import_strings(new_imports)
172
216
 
173
217
  # Find old import strings so we can compare with the new import strings
174
218
  # and see if anything has changed.
175
219
  old_import_strings = []
176
- old_imports_lines.times do |line|
177
- old_import_strings << @editor.read_line(1 + line + imports_start_at)
220
+ (imports_start_at...imports_end_at).each do |line_index|
221
+ old_import_strings << @editor.read_line(line_index + 1)
178
222
  end
179
223
 
180
224
  # If nothing has changed, bail to prevent unnecessarily dirtying the
@@ -187,29 +231,61 @@ module ImportJS
187
231
  # We need to add each line individually because the Vim buffer will
188
232
  # convert newline characters to `~@`.
189
233
  import_string.split("\n").reverse_each do |line|
190
- @editor.append_line(0 + imports_start_at, line)
234
+ @editor.append_line(imports_start_at, line)
191
235
  end
192
236
  end
193
237
  end
194
238
 
195
- # @return [Hash]
196
- def find_current_imports
197
- potential_import_lines = []
198
- @editor.count_lines.times do |n|
199
- line = @editor.read_line(n + 1)
200
- break if line.strip.empty?
201
- potential_import_lines << line
239
+ # @return [Number]
240
+ def find_imports_start_line_index
241
+ imports_start_line_index = 0
242
+
243
+ # Skip over things at the top, like "use strict" and comments.
244
+ inside_multi_line_comment = false
245
+ matched_non_whitespace_line = false
246
+ (0...@editor.count_lines).each do |line_index|
247
+ line = @editor.read_line(line_index + 1)
248
+
249
+ if inside_multi_line_comment || line =~ REGEX_MULTI_LINE_COMMENT_START
250
+ matched_non_whitespace_line = true
251
+ imports_start_line_index = line_index + 1
252
+ inside_multi_line_comment = !(line =~ REGEX_MULTI_LINE_COMMENT_END)
253
+ next
254
+ end
255
+
256
+ if line =~ REGEX_USE_STRICT || line =~ REGEX_SINGLE_LINE_COMMENT
257
+ matched_non_whitespace_line = true
258
+ imports_start_line_index = line_index + 1
259
+ next
260
+ end
261
+
262
+ if line =~ REGEX_WHITESPACE_ONLY
263
+ imports_start_line_index = line_index + 1
264
+ next
265
+ end
266
+
267
+ break
202
268
  end
203
269
 
270
+ # We don't want to skip over blocks that are only whitespace
271
+ return imports_start_line_index if matched_non_whitespace_line
272
+ 0
273
+ end
274
+
275
+ # @return [Hash]
276
+ def find_current_imports
204
277
  result = {
205
278
  imports: [],
206
279
  newline_count: 0,
207
- imports_start_at: 0
280
+ imports_start_at: find_imports_start_line_index,
208
281
  }
209
282
 
210
- if potential_import_lines[0] =~ /(['"])use strict\1;?/
211
- result[:imports_start_at] = 1
212
- potential_import_lines.shift
283
+ # Find block of lines that might be imports.
284
+ potential_import_lines = []
285
+ (result[:imports_start_at]...@editor.count_lines).each do |line_index|
286
+ line = @editor.read_line(line_index + 1)
287
+ break if line.strip.empty?
288
+ potential_import_lines << line
213
289
  end
214
290
 
215
291
  # We need to put the potential imports back into a blob in order to scan
@@ -224,7 +300,7 @@ module ImportJS
224
300
  break unless import_statement
225
301
 
226
302
  if imports[import_statement.path]
227
- # Import already exists, so this line is likely one of a destructuring
303
+ # Import already exists, so this line is likely one of a named imports
228
304
  # pair. Combine it into the same ImportStatement.
229
305
  imports[import_statement.path].merge(import_statement)
230
306
  else
@@ -242,64 +318,81 @@ module ImportJS
242
318
  # @return [Array]
243
319
  def find_js_modules(variable_name)
244
320
  path_to_current_file = @editor.path_to_current_file
245
- if alias_module = @config.resolve_alias(variable_name,
246
- path_to_current_file)
247
- return [alias_module]
248
- end
321
+
322
+ alias_module = @config.resolve_alias(variable_name, path_to_current_file)
323
+ return [alias_module] if alias_module
324
+
325
+ named_imports_module = @config.resolve_named_exports(variable_name)
326
+ return [named_imports_module] if named_imports_module
327
+
328
+ formatted_var_name = formatted_to_regex(variable_name)
249
329
  egrep_command =
250
- "egrep -i \"(/|^)#{formatted_to_regex(variable_name)}(/index)?(/package)?\.js.*\""
330
+ "egrep -i \"(/|^)#{formatted_var_name}(/index)?(/package)?\.js.*\""
251
331
  matched_modules = []
252
332
  @config.get('lookup_paths').each do |lookup_path|
333
+ if lookup_path == ''
334
+ # If lookup_path is an empty string, the `find` command will not work
335
+ # as desired so we bail early.
336
+ fail ImportJS::FindError.new,
337
+ "lookup path cannot be empty (#{lookup_path.inspect})"
338
+ end
339
+
253
340
  find_command = %W[
254
341
  find #{lookup_path}
255
342
  -name "**.js*"
256
343
  -not -path "./node_modules/*"
257
344
  ].join(' ')
258
- out, _ = Open3.capture3("#{find_command} | #{egrep_command}")
345
+ command = "#{find_command} | #{egrep_command}"
346
+ out, err = Open3.capture3(command)
347
+
348
+ fail ImportJS::FindError.new, err unless err == ''
349
+
259
350
  matched_modules.concat(
260
351
  out.split("\n").map do |f|
261
352
  next if @config.get('excludes').any? do |glob_pattern|
262
353
  File.fnmatch(glob_pattern, f)
263
354
  end
264
- js_module = ImportJS::JSModule.new(
355
+ ImportJS::JSModule.construct(
265
356
  lookup_path: lookup_path,
266
357
  relative_file_path: f,
267
- strip_file_extensions: @config.get('strip_file_extensions'),
268
- make_relative_to: @config.get('use_relative_paths') &&
269
- path_to_current_file
358
+ strip_file_extensions:
359
+ @config.get('strip_file_extensions', from_file: f),
360
+ make_relative_to:
361
+ @config.get('use_relative_paths', from_file: f) &&
362
+ path_to_current_file,
363
+ strip_from_path:
364
+ @config.get('strip_from_path', from_file: f)
270
365
  )
271
-
272
- next if js_module.skip
273
- js_module
274
366
  end.compact
275
367
  )
276
368
  end
277
369
 
278
370
  # Find imports from package.json
371
+ ignore_prefixes = @config.get('ignore_package_prefixes').map do |prefix|
372
+ Regexp.escape(prefix)
373
+ end
374
+ dep_regex = /^(?:#{ignore_prefixes.join('|')})?#{formatted_var_name}$/
375
+
279
376
  @config.package_dependencies.each do |dep|
280
- ignore_prefixes = @config.get('ignore_package_prefixes')
281
- dep_matcher = /^#{formatted_to_regex(variable_name)}$/
282
- if dep =~ dep_matcher ||
283
- ignore_prefixes.any? do |prefix|
284
- dep.sub(/^#{prefix}/, '') =~ dep_matcher
285
- end
286
- js_module = ImportJS::JSModule.new(
287
- lookup_path: 'node_modules',
288
- relative_file_path: "node_modules/#{dep}/package.json",
289
- strip_file_extensions: [])
290
- next if js_module.skip
291
- matched_modules << js_module
292
- end
377
+ next unless dep =~ dep_regex
378
+
379
+ js_module = ImportJS::JSModule.construct(
380
+ lookup_path: 'node_modules',
381
+ relative_file_path: "node_modules/#{dep}/package.json",
382
+ strip_file_extensions: [])
383
+ matched_modules << js_module if js_module
293
384
  end
294
385
 
295
386
  # If you have overlapping lookup paths, you might end up seeing the same
296
387
  # module to import twice. In order to dedupe these, we remove the module
297
388
  # with the longest path
298
- matched_modules.sort do |a, b|
389
+ matched_modules.sort! do |a, b|
299
390
  a.import_path.length <=> b.import_path.length
300
- end.uniq do |m|
391
+ end
392
+ matched_modules.uniq! do |m|
301
393
  m.lookup_path + '/' + m.import_path
302
- end.sort do |a, b|
394
+ end
395
+ matched_modules.sort! do |a, b|
303
396
  a.display_name <=> b.display_name
304
397
  end
305
398
  end
@@ -309,8 +402,15 @@ module ImportJS
309
402
  # @return [String]
310
403
  def resolve_one_js_module(js_modules, variable_name)
311
404
  if js_modules.length == 1
312
- message("Imported `#{js_modules.first.display_name}` #{timing}")
313
- return js_modules.first
405
+ js_module = js_modules.first
406
+ js_module_name = js_module.display_name
407
+ imported = if js_module.has_named_exports
408
+ "`#{variable_name}` from `#{js_module_name}`"
409
+ else
410
+ "`#{js_module_name}`"
411
+ end
412
+ message("Imported #{imported} #{timing}")
413
+ return js_module
314
414
  end
315
415
 
316
416
  selected_index = @editor.ask_for_selection(
@@ -356,9 +456,34 @@ module ImportJS
356
456
  .downcase
357
457
  end
358
458
 
459
+ def time
460
+ timing = { start: Time.now }
461
+ yield
462
+ timing[:end] = Time.now
463
+ @timing = timing
464
+ end
465
+
359
466
  # @return [String]
360
467
  def timing
361
468
  "(#{(@timing[:end] - @timing[:start]).round(2)}s)"
362
469
  end
470
+
471
+ def maintain_cursor_position
472
+ # Save editor information before modifying the buffer so we can put the
473
+ # cursor in the correct spot after modifying the buffer.
474
+ current_row, current_col = @editor.cursor
475
+ old_buffer_lines = @editor.count_lines
476
+
477
+ # Yield to a block that will potentially modify the buffer.
478
+ yield
479
+
480
+ # Check to see if lines were added or removed.
481
+ lines_changed = @editor.count_lines - old_buffer_lines
482
+ return unless lines_changed
483
+
484
+ # Lines were added or removed, so we want to adjust the cursor position to
485
+ # match.
486
+ @editor.cursor = [current_row + lines_changed, current_col]
487
+ end
363
488
  end
364
489
  end