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.
- checksums.yaml +4 -4
- data/bin/import-js +2 -2
- data/lib/import_js/command_line_editor.rb +4 -9
- data/lib/import_js/configuration.rb +65 -39
- data/lib/import_js/emacs_editor.rb +137 -137
- data/lib/import_js/import_statement.rb +81 -76
- data/lib/import_js/importer.rb +227 -102
- data/lib/import_js/js_module.rb +97 -54
- data/lib/import_js/version.rb +3 -1
- data/lib/import_js/vim_editor.rb +2 -2
- data/lib/import_js.rb +8 -0
- metadata +1 -1
data/lib/import_js/importer.rb
CHANGED
@@ -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.
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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.
|
41
|
-
|
42
|
+
@config = ImportJS::Configuration.new(@editor.path_to_current_file)
|
43
|
+
js_modules = []
|
42
44
|
variable_name = @editor.current_word
|
43
|
-
|
44
|
-
|
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
|
-
|
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.
|
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 =
|
61
|
-
|
62
|
-
|
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
|
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
|
-
|
76
|
-
|
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 =
|
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 =~
|
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 =~
|
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
|
-
|
123
|
-
|
124
|
-
|
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
|
173
|
+
import = imports.find do |an_import|
|
174
|
+
an_import.path == js_module.import_path
|
175
|
+
end
|
140
176
|
|
141
177
|
if import
|
142
|
-
|
143
|
-
|
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.
|
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(
|
162
|
-
@editor.append_line(
|
211
|
+
!@editor.read_line(imports_end_at + 1).strip.empty?
|
212
|
+
@editor.append_line(imports_end_at, '')
|
163
213
|
end
|
164
214
|
|
165
|
-
|
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
|
-
|
177
|
-
old_import_strings << @editor.read_line(
|
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(
|
234
|
+
@editor.append_line(imports_start_at, line)
|
191
235
|
end
|
192
236
|
end
|
193
237
|
end
|
194
238
|
|
195
|
-
# @return [
|
196
|
-
def
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
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:
|
280
|
+
imports_start_at: find_imports_start_line_index,
|
208
281
|
}
|
209
282
|
|
210
|
-
|
211
|
-
|
212
|
-
|
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
|
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
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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 \"(/|^)#{
|
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
|
-
|
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
|
-
|
355
|
+
ImportJS::JSModule.construct(
|
265
356
|
lookup_path: lookup_path,
|
266
357
|
relative_file_path: f,
|
267
|
-
strip_file_extensions:
|
268
|
-
|
269
|
-
|
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
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
391
|
+
end
|
392
|
+
matched_modules.uniq! do |m|
|
301
393
|
m.lookup_path + '/' + m.import_path
|
302
|
-
end
|
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
|
-
|
313
|
-
|
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
|