import_js 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3808c953be3c8a47d0212d952f35b4329774cba8
4
- data.tar.gz: f1929ccab0f99004521babc2dbbf8d35347c7313
3
+ metadata.gz: eddd4e67b47ab663fb4bb1fc39a2e11810f9f640
4
+ data.tar.gz: 399fee47fa49f677e9646ae1451a897869a441cc
5
5
  SHA512:
6
- metadata.gz: 9380c3f44a4306a0e9d843496bf846c47200e696649a88348135e9c7f1d928e04ad5520d67a7c15beb3306e02af1ad7aa2fd2aaaef983c0eec2463e6ca9fd288
7
- data.tar.gz: cc46051020022e9e7ba9db5b4c8750b104ad372bb72442f5fb7712867084f8bfff92f9dcba06826ca77d87a42d4897dfe72b5b96b7fb89f7dcb61d804e684f75
6
+ metadata.gz: d3269f091d4da05e069fd26f935450964495da18d1a970a77cacc6bea9fc5593c002c16c3cb53a2b8234ed2be711018aefbfb03e58e5b76e04cfd4b383ee9513
7
+ data.tar.gz: 545f69bfb8caadcc9bd30e3ae04ab73640d369272b47997be3e5c8718fee06f4f7f740fa9e608fa55872c4f606d04a8af9c9ff804867485ed7451a85714e4b8f
@@ -5,22 +5,45 @@ require 'slop'
5
5
  require 'json'
6
6
 
7
7
  opts = Slop.parse do |o|
8
- o.string '-w', '--word', 'a word/variable to import'
9
- o.bool '--goto', 'instead of importing, just print the path to a module'
10
- o.array '--selections', 'a list of resolved selections, e.g. Foo:0,Bar:1'
11
- o.string '--filename',
12
- 'a path to the file which contents are being passed in as stdin'
13
- o.on '-v', '--version', 'print the current version' do
8
+ o.banner = 'Usage: import-js [<path-to-file>] [options] ...'
9
+ o.string '-w', '--word', 'A word/variable to import'
10
+ o.bool '--goto', 'Instead of importing, just print the path to a module'
11
+ o.array '--selections', 'A list of resolved selections, e.g. Foo:0,Bar:1'
12
+ o.string '--stdin-file-path',
13
+ 'A path to the file whose content is being passed in as stdin. ' \
14
+ 'This is used as a way to make sure that the right configuration ' \
15
+ 'is applied.'
16
+ o.bool '--overwrite',
17
+ 'Overwrite the file with the result after importing (the default ' \
18
+ 'behavior is to print to stdout). This only applies if you are ' \
19
+ 'passing in a file (<path-to-file>) as the first positional argument.'
20
+ o.string '--filename', '(deprecated) Alias for --stdin-file-path'
21
+ o.bool '--rewrite',
22
+ 'Rewrite all current imports to match Import-JS configuration. ' \
23
+ 'This does not add missing imports or remove unused imports.'
24
+
25
+ o.on '-v', '--version', 'Prints the current version' do
14
26
  puts ImportJS::VERSION
15
27
  exit
16
28
  end
17
- o.on '-h', '--help', 'prints help' do
29
+ o.on '-h', '--help', 'Prints help' do
18
30
  puts o
19
31
  exit
20
32
  end
21
33
  end
22
34
 
23
- file_contents = STDIN.read.split("\n")
35
+ path_to_file = opts.arguments[0] || opts['stdin-file-path'] || opts[:filename]
36
+
37
+ file_contents = if STDIN.tty?
38
+ unless path_to_file
39
+ puts 'Error: missing <path-to-file>'
40
+ puts opts
41
+ exit 1
42
+ end
43
+ File.read(path_to_file).split("\n")
44
+ else
45
+ STDIN.read.split("\n")
46
+ end
24
47
 
25
48
  if opts[:selections]
26
49
  # Convert array of string tuples to hash, `word` => `selectedIndex`
@@ -30,12 +53,15 @@ if opts[:selections]
30
53
  end]
31
54
  end
32
55
 
33
- editor = ImportJS::CommandLineEditor.new(file_contents, opts)
56
+ editor = ImportJS::CommandLineEditor.new(
57
+ file_contents, opts.to_hash.merge(path_to_file: path_to_file))
34
58
  importer = ImportJS::Importer.new(editor)
35
59
  if opts.goto?
36
60
  importer.goto
37
61
  elsif opts[:word]
38
62
  importer.import
63
+ elsif opts[:rewrite]
64
+ importer.rewrite_imports
39
65
  else
40
66
  importer.fix_imports
41
67
  end
@@ -43,6 +69,10 @@ end
43
69
  if opts.goto?
44
70
  # Print the path to the module to go to
45
71
  puts editor.goto
72
+ elsif opts[:overwrite]
73
+ File.open(path_to_file, 'w') do |f|
74
+ f.write editor.current_file_content + "\n"
75
+ end
46
76
  else
47
77
  # Print resulting file to stdout
48
78
  puts editor.current_file_content
@@ -4,15 +4,15 @@ module ImportJS
4
4
  # under that namespace, e.g. `ImportJS::Importer`.
5
5
 
6
6
  class ParseError < StandardError
7
- # Error thrown when a JS file can't be parsed
7
+ # Error raised when a JS file can't be parsed
8
8
  end
9
9
 
10
10
  class FindError < StandardError
11
- # Error thrown when the find command fails
11
+ # Error raised when the find command fails
12
12
  end
13
13
 
14
14
  class ClientTooOldError < StandardError
15
- # Error thrown when the client is too old to handle the config
15
+ # Error raised when the client is too old to handle the config
16
16
  end
17
17
  end
18
18
 
@@ -20,6 +20,7 @@ require_relative 'import_js/command_line_editor'
20
20
  require_relative 'import_js/configuration'
21
21
  require_relative 'import_js/emacs_editor'
22
22
  require_relative 'import_js/import_statement'
23
+ require_relative 'import_js/import_statements'
23
24
  require_relative 'import_js/importer'
24
25
  require_relative 'import_js/js_module'
25
26
  require_relative 'import_js/version'
@@ -7,7 +7,7 @@ module ImportJS
7
7
  @ask_for_selections = []
8
8
  @selections = opts[:selections] unless opts[:selections].empty?
9
9
  @word = opts[:word]
10
- @filename = opts[:filename]
10
+ @path_to_file = opts[:path_to_file]
11
11
  end
12
12
 
13
13
  # @return [String]
@@ -17,7 +17,7 @@ module ImportJS
17
17
 
18
18
  # @return [String?]
19
19
  def path_to_current_file
20
- @filename
20
+ @path_to_file
21
21
  end
22
22
 
23
23
  # @param file_path [String]
@@ -110,17 +110,5 @@ module ImportJS
110
110
  nil
111
111
  end
112
112
  end
113
-
114
- # Get the preferred max length of a line.
115
- # @return [Number?]
116
- def max_line_length
117
- 80
118
- end
119
-
120
- # @return [String] shiftwidth number of spaces if expandtab is not set,
121
- # otherwise `\t`.
122
- def tab
123
- ' '
124
- end
125
113
  end
126
114
  end
@@ -15,9 +15,11 @@ module ImportJS
15
15
  'import_dev_dependencies' => false,
16
16
  'import_function' => 'require',
17
17
  'lookup_paths' => ['.'],
18
+ 'max_line_length' => 80,
18
19
  'minimum_version' => '0.0.0',
19
20
  'strip_file_extensions' => ['.js', '.jsx'],
20
21
  'strip_from_path' => nil,
22
+ 'tab' => ' ',
21
23
  'use_relative_paths' => false,
22
24
  }.freeze
23
25
 
@@ -110,9 +112,9 @@ module ImportJS
110
112
  return if Gem::Dependency.new('', ">= #{minimum_version}")
111
113
  .match?('', VERSION)
112
114
 
113
- fail ClientTooOldError,
114
- 'The .importjs.json file you are using requires version ' \
115
- "#{get('minimum_version')}. You are using #{VERSION}."
115
+ raise ClientTooOldError,
116
+ 'The .importjs.json file you are using requires version ' \
117
+ "#{get('minimum_version')}. You are using #{VERSION}."
116
118
  end
117
119
  end
118
120
  end
@@ -138,22 +138,5 @@ module ImportJS
138
138
  return if selected_index < 1
139
139
  selected_index - 1
140
140
  end
141
-
142
- # Get the preferred max length of a line
143
- # @return [Number?]
144
- def max_line_length
145
- 80
146
- end
147
-
148
- # @return [String] shiftwidth number of spaces if expandtab is not set,
149
- # otherwise `\t`
150
- def tab
151
- ' ' * shift_width || 2
152
- end
153
-
154
- # @return [Number?]
155
- def shift_width
156
- 2
157
- end
158
141
  end
159
142
  end
@@ -9,12 +9,13 @@ module ImportJS
9
9
  (?<declaration_keyword>const|let|var)\s+ # <declaration_keyword>
10
10
  (?<assignment>.+?) # <assignment> variable assignment
11
11
  \s*=\s*
12
- (?<import_function>[^\(]+?)\( # <import_function> variable assignment
12
+ (?<import_function>\w+?)\( # <import_function> variable assignment
13
13
  (?<quote>'|") # <quote> opening quote
14
- (?<path>[^\2]+) # <path> module path
14
+ (?<path>[^\2\n]+) # <path> module path
15
15
  \k<quote> # closing quote
16
16
  \);?
17
17
  \s*
18
+ \Z
18
19
  /xm
19
20
 
20
21
  REGEX_IMPORT = /
@@ -23,12 +24,14 @@ module ImportJS
23
24
  (?<assignment>.*?) # <assignment> variable assignment
24
25
  \s+from\s+
25
26
  (?<quote>'|") # <quote> opening quote
26
- (?<path>[^\2]+) # <path> module path
27
+ (?<path>[^\2\n]+) # <path> module path
27
28
  \k<quote> # closing quote
28
29
  ;?\s*
30
+ \Z
29
31
  /xm
30
32
 
31
33
  REGEX_NAMED = /
34
+ \A
32
35
  (?: # non-capturing group
33
36
  (?<default>.*?) # <default> default import
34
37
  ,\s*
@@ -38,6 +41,7 @@ module ImportJS
38
41
  (?<named>.*) # <named> named imports
39
42
  \s*
40
43
  \}
44
+ \Z
41
45
  /xm
42
46
 
43
47
  attr_accessor :assignment
@@ -59,50 +63,67 @@ module ImportJS
59
63
  REGEX_IMPORT.match(string)
60
64
  return unless match
61
65
 
62
- statement = new
63
- statement.original_import_string = match.string
64
- statement.declaration_keyword = match[:declaration_keyword]
65
- statement.path = match[:path]
66
- statement.assignment = match[:assignment]
67
- if match.names.include? 'import_function'
68
- statement.import_function = match[:import_function]
69
- end
70
- dest_match = statement.assignment.match(REGEX_NAMED)
66
+ import_function = if match.names.include?('import_function')
67
+ match[:import_function]
68
+ else
69
+ 'require'
70
+ end
71
+
72
+ dest_match = match[:assignment].match(REGEX_NAMED)
71
73
  if dest_match
72
- statement.default_import = dest_match[:default]
73
- statement.named_imports =
74
- dest_match[:named].split(/,\s*/).map(&:strip)
74
+ default_import = dest_match[:default]
75
+ named_imports = dest_match[:named].split(/,\s*/).map(&:strip)
75
76
  else
76
- statement.default_import = statement.assignment
77
+ default_import = match[:assignment]
78
+ return unless default_import =~ /\A\w+\Z/
77
79
  end
78
- statement
79
- end
80
80
 
81
- # Sets the default_import and clears the original import string cache.
82
- # @param value [String]
83
- def set_default_import(value)
84
- @default_import = value
85
- clear_import_string_cache
81
+ new(
82
+ assignment: match[:assignment],
83
+ declaration_keyword: match[:declaration_keyword],
84
+ default_import: default_import,
85
+ import_function: import_function,
86
+ named_imports: named_imports,
87
+ original_import_string: match.string,
88
+ path: match[:path]
89
+ )
86
90
  end
87
91
 
88
- # Injects a new variable into an already existing set of named imports.
89
- # @param variable_name [String]
90
- def inject_named_import(variable_name)
91
- @named_imports ||= []
92
- named_imports << variable_name
93
- named_imports.sort!.uniq!
94
-
95
- clear_import_string_cache
92
+ def initialize(
93
+ assignment: nil,
94
+ declaration_keyword: nil,
95
+ default_import: nil,
96
+ import_function: nil,
97
+ named_imports: nil,
98
+ original_import_string: nil,
99
+ path: nil
100
+ )
101
+ @assignment = assignment
102
+ @declaration_keyword = declaration_keyword
103
+ @default_import = default_import
104
+ @import_function = import_function
105
+ @named_imports = named_imports
106
+ @original_import_string = original_import_string
107
+ @path = path
96
108
  end
97
109
 
98
110
  # Deletes a variable from an already existing default import or set of
99
- # named imports.
111
+ # named imports.
100
112
  # @param variable_name [String]
101
- def delete_variable(variable_name)
102
- @default_import = nil if default_import == variable_name
103
- @named_imports.delete(variable_name) if named_imports?
113
+ def delete_variable!(variable_name)
114
+ touched = false
104
115
 
105
- clear_import_string_cache
116
+ if default_import == variable_name
117
+ @default_import = nil
118
+ touched = true
119
+ end
120
+
121
+ if named_imports?
122
+ deleted = @named_imports.delete(variable_name)
123
+ touched = true if deleted
124
+ end
125
+
126
+ clear_import_string_cache if touched
106
127
  end
107
128
 
108
129
  # @return [Boolean] true if there are named imports
@@ -116,39 +137,38 @@ module ImportJS
116
137
  default_import.nil? && !named_imports?
117
138
  end
118
139
 
119
- # @return [Array] an array that can be used in `uniq!` to dedupe equal
120
- # statements, e.g.
121
- # `const foo = require('foo');`
122
- # `import foo from 'foo';`
140
+ # @return [Boolean] true if this instance was created through parsing an
141
+ # existing import and it hasn't been altered since it was created.
142
+ def parsed_and_untouched?
143
+ !original_import_string.nil?
144
+ end
145
+
146
+ # @return [Array] an array that can be used in `sort` and `uniq`
123
147
  def to_normalized
124
- [default_import, named_imports, path]
148
+ [default_import || '', named_imports || '']
149
+ end
150
+
151
+ # @return [Array<String>] Array of all variables that this ImportStatement
152
+ # imports.
153
+ def variables
154
+ [@default_import, *@named_imports].compact
125
155
  end
126
156
 
127
157
  # @param max_line_length [Number] where to cap lines at
128
158
  # @param tab [String] e.g. ' ' (two spaces)
129
- # @return [Array] generated import statement strings
159
+ # @return [Array<String>] generated import statement strings
130
160
  def to_import_strings(max_line_length, tab)
131
161
  return [original_import_string] if original_import_string
132
162
 
133
163
  if declaration_keyword == 'import'
134
164
  # ES2015 Modules (ESM) syntax can support default imports and
135
165
  # named imports on the same line.
136
- if named_imports?
137
- [named_import_string(max_line_length, tab)]
138
- else
139
- [default_import_string(max_line_length, tab)]
140
- end
166
+ return [named_import_string(max_line_length, tab)] if named_imports?
167
+ [default_import_string(max_line_length, tab)]
141
168
  else # const/var
142
169
  strings = []
143
-
144
- if default_import
145
- strings << default_import_string(max_line_length, tab)
146
- end
147
-
148
- if named_imports?
149
- strings << named_import_string(max_line_length, tab)
150
- end
151
-
170
+ strings << default_import_string(max_line_length, tab) if default_import
171
+ strings << named_import_string(max_line_length, tab) if named_imports?
152
172
  strings
153
173
  end
154
174
  end
@@ -156,15 +176,22 @@ module ImportJS
156
176
  # Merge another ImportStatement into this one.
157
177
  # @param import_statement [ImportJS::ImportStatement]
158
178
  def merge(import_statement)
159
- if import_statement.default_import
179
+ if import_statement.default_import &&
180
+ @default_import != import_statement.default_import
160
181
  @default_import = import_statement.default_import
161
182
  clear_import_string_cache
162
183
  end
163
184
 
164
185
  if import_statement.named_imports?
165
186
  @named_imports ||= []
187
+ original_named_imports = @named_imports.clone
166
188
  @named_imports.concat(import_statement.named_imports)
167
189
  @named_imports.sort!.uniq!
190
+ clear_import_string_cache if original_named_imports != @named_imports
191
+ end
192
+
193
+ if @declaration_keyword != import_statement.declaration_keyword
194
+ @declaration_keyword = import_statement.declaration_keyword
168
195
  clear_import_string_cache
169
196
  end
170
197
  end
@@ -181,7 +208,7 @@ module ImportJS
181
208
  # @return [Array]
182
209
  def equals_and_value
183
210
  return ['from', "'#{path}';"] if declaration_keyword == 'import'
184
- ['=', "#{import_function}('#{path}');"]
211
+ ['=', "#{@import_function}('#{path}');"]
185
212
  end
186
213
 
187
214
  # @param max_line_length [Number] where to cap lines at
@@ -189,10 +216,10 @@ module ImportJS
189
216
  # @return [String] import statement, wrapped at max line length if necessary
190
217
  def default_import_string(max_line_length, tab)
191
218
  equals, value = equals_and_value
192
- line = "#{declaration_keyword} #{default_import} #{equals} #{value}"
219
+ line = "#{@declaration_keyword} #{@default_import} #{equals} #{value}"
193
220
  return line unless line_too_long?(line, max_line_length)
194
221
 
195
- "#{declaration_keyword} #{default_import} #{equals}\n#{tab}#{value}"
222
+ "#{@declaration_keyword} #{@default_import} #{equals}\n#{tab}#{value}"
196
223
  end
197
224
 
198
225
  # @param max_line_length [Number] where to cap lines at
@@ -200,17 +227,17 @@ module ImportJS
200
227
  # @return [String] import statement, wrapped at max line length if necessary
201
228
  def named_import_string(max_line_length, tab)
202
229
  equals, value = equals_and_value
203
- if declaration_keyword == 'import' && default_import
204
- prefix = "#{default_import}, "
230
+ if @declaration_keyword == 'import' && @default_import
231
+ prefix = "#{@default_import}, "
205
232
  end
206
233
 
207
- named = "{ #{named_imports.join(', ')} }"
208
- line = "#{declaration_keyword} #{prefix}#{named} #{equals} " \
234
+ named = "{ #{@named_imports.join(', ')} }"
235
+ line = "#{@declaration_keyword} #{prefix}#{named} #{equals} " \
209
236
  "#{value}"
210
237
  return line unless line_too_long?(line, max_line_length)
211
238
 
212
- named = "{\n#{tab}#{named_imports.join(",\n#{tab}")},\n}"
213
- "#{declaration_keyword} #{prefix}#{named} #{equals} #{value}"
239
+ named = "{\n#{tab}#{@named_imports.join(",\n#{tab}")},\n}"
240
+ "#{@declaration_keyword} #{prefix}#{named} #{equals} #{value}"
214
241
  end
215
242
 
216
243
  def clear_import_string_cache
@@ -0,0 +1,175 @@
1
+ module ImportJS
2
+ # Class that sorts ImportStatements as they are pushed in
3
+ class ImportStatements
4
+ include Enumerable
5
+
6
+ STYLE_IMPORT = :import
7
+ STYLE_CONST = :const
8
+ STYLE_VAR = :var
9
+ STYLE_CUSTOM = :custom
10
+
11
+ # Order is significant here
12
+ STYLES = [STYLE_IMPORT, STYLE_CONST, STYLE_VAR, STYLE_CUSTOM].freeze
13
+
14
+ PATH_TYPE_PACKAGE = :package
15
+ PATH_TYPE_NON_RELATIVE = :non_relative
16
+ PATH_TYPE_RELATIVE = :relative
17
+
18
+ # Order is significant here
19
+ PATH_TYPES = [
20
+ PATH_TYPE_PACKAGE,
21
+ PATH_TYPE_NON_RELATIVE,
22
+ PATH_TYPE_RELATIVE,
23
+ ].freeze
24
+
25
+ GROUPINGS_ARRAY = STYLES.map do |style|
26
+ PATH_TYPES.map do |location|
27
+ "#{style} #{location}"
28
+ end
29
+ end.flatten.freeze
30
+
31
+ GROUPINGS = Hash[
32
+ GROUPINGS_ARRAY.each_with_index.map { |group, index| [group, index] }
33
+ ].freeze
34
+
35
+ # @param config [ImportJS::Configuration]
36
+ # @param imports [Hash]
37
+ def initialize(config, imports = {})
38
+ @config = config
39
+ @imports = imports
40
+ end
41
+
42
+ def each
43
+ return enum_for(:each) unless block_given?
44
+
45
+ @imports.each do |_, import_statement|
46
+ yield import_statement
47
+ end
48
+ end
49
+
50
+ def clone
51
+ ImportStatements.new(@config, @imports.clone)
52
+ end
53
+
54
+ # @param import_statement [ImportJS::ImportStatement]
55
+ # @return [ImportJS::ImportStatements]
56
+ def push(import_statement)
57
+ if @imports[import_statement.path]
58
+ # Import already exists, so this line is likely one of a named imports
59
+ # pair. Combine it into the same ImportStatement.
60
+ @imports[import_statement.path].merge(import_statement)
61
+ else
62
+ # This is a new import, so we just add it to the hash.
63
+ @imports[import_statement.path] = import_statement
64
+ end
65
+
66
+ self # for chaining
67
+ end
68
+ alias << push
69
+
70
+ # @param variable_names [Array<String>]
71
+ # @return [ImportJS::ImportStatements]
72
+ def delete_variables!(variable_names)
73
+ @imports.reject! do |_, import_statement|
74
+ variable_names.each do |variable_name|
75
+ import_statement.delete_variable!(variable_name)
76
+ end
77
+ import_statement.empty?
78
+ end
79
+
80
+ self # for chaining
81
+ end
82
+
83
+ # Convert the import statements into an array of strings, with an empty
84
+ # string between each group.
85
+ # @return [Array<String>]
86
+ def to_a
87
+ max_line_length = @config.get('max_line_length')
88
+ tab = @config.get('tab')
89
+
90
+ strings = []
91
+ to_groups.each do |group|
92
+ group.each do |import_statement|
93
+ strings.concat(
94
+ import_statement.to_import_strings(max_line_length, tab))
95
+ end
96
+ strings << '' # Add a blank line between groups.
97
+ end
98
+
99
+ # We don't want to include a trailing newline at the end of all the
100
+ # groups here.
101
+ strings.pop if strings.last == ''
102
+
103
+ strings
104
+ end
105
+
106
+ private
107
+
108
+ # Sort the import statements by path and group them based on our heuristic
109
+ # of style and path type.
110
+ # @return [Array<Array<ImportJS::ImportStatement>>]
111
+ def to_groups
112
+ groups = []
113
+
114
+ imports_array = @imports.values
115
+
116
+ # There's a chance we have duplicate imports (can happen when switching
117
+ # declaration_keyword for instance). By first sorting imports so that new
118
+ # ones are first, then removing duplicates, we guarantee that we delete
119
+ # the old ones that are now redundant.
120
+ partitioned = imports_array.partition do |import_statement|
121
+ !import_statement.parsed_and_untouched?
122
+ end.flatten.uniq(&:to_normalized).sort_by(&:to_normalized)
123
+
124
+ package_dependencies = @config.package_dependencies
125
+ partitioned.each do |import_statement|
126
+ # Figure out what group to put this import statement in
127
+ group_index = import_statement_group_index(
128
+ import_statement, package_dependencies)
129
+
130
+ # Add the import statement to the group
131
+ groups[group_index] ||= []
132
+ groups[group_index] << import_statement
133
+ end
134
+
135
+ groups.compact! unless groups.empty?
136
+ groups
137
+ end
138
+
139
+ # @param import_statement [ImportJS::ImportStatement]
140
+ # @param package_dependencies [Array<String>]
141
+ # @return [Number]
142
+ def import_statement_group_index(import_statement, package_dependencies)
143
+ style = import_statement_style(import_statement)
144
+ path_type = import_statement_path_type(
145
+ import_statement, package_dependencies)
146
+
147
+ GROUPINGS["#{style} #{path_type}"]
148
+ end
149
+
150
+ # Determine import statement style
151
+ # @param import_statement [ImportJS::ImportStatement]
152
+ # @return [String] 'import', 'const', 'var', or 'custom'
153
+ def import_statement_style(import_statement)
154
+ return STYLE_IMPORT if import_statement.declaration_keyword == 'import'
155
+
156
+ if import_statement.import_function == 'require'
157
+ return STYLE_CONST if import_statement.declaration_keyword == 'const'
158
+ return STYLE_VAR if import_statement.declaration_keyword == 'var'
159
+ end
160
+
161
+ STYLE_CUSTOM
162
+ end
163
+
164
+ # Determine import path type
165
+ # @param import_statement [ImportJS::ImportStatement]
166
+ # @param package_dependencies [Array<String>]
167
+ # @return [String] 'package, 'non-relative', 'relative'
168
+ def import_statement_path_type(import_statement, package_dependencies)
169
+ path = import_statement.path
170
+ return PATH_TYPE_RELATIVE if path.start_with?('.')
171
+ return PATH_TYPE_PACKAGE if package_dependencies.include?(path)
172
+ PATH_TYPE_NON_RELATIVE
173
+ end
174
+ end
175
+ end
@@ -1,14 +1,10 @@
1
1
  require 'json'
2
2
  require 'open3'
3
+ require 'set'
4
+ require 'strscan'
3
5
 
4
6
  module ImportJS
5
7
  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
-
12
8
  def initialize(editor = VIMEditor.new)
13
9
  @editor = editor
14
10
  end
@@ -31,10 +27,9 @@ module ImportJS
31
27
 
32
28
  maintain_cursor_position do
33
29
  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])
30
+ import_statement = js_module.to_import_statement(variable_name, @config)
31
+ old_imports[:imports] << import_statement
32
+ replace_imports(old_imports[:range], old_imports[:imports])
38
33
  end
39
34
  end
40
35
 
@@ -43,16 +38,11 @@ module ImportJS
43
38
  js_modules = []
44
39
  variable_name = @editor.current_word
45
40
  time do
46
- js_modules = find_js_modules(variable_name)
47
- end
48
-
49
- if js_modules.empty?
50
- # No JS modules are found for the variable, so there is nothing to go to
51
- # and we return early.
52
- return message("No modules were found for `#{variable_name}`")
41
+ js_modules = find_js_modules_for(variable_name)
53
42
  end
54
43
 
55
- js_module = resolve_goto_module(js_modules, variable_name)
44
+ js_module = resolve_module_using_current_imports(
45
+ js_modules, variable_name)
56
46
 
57
47
  unless js_module
58
48
  # The current word is not mappable to one of the JS modules that we
@@ -83,38 +73,48 @@ module ImportJS
83
73
  reload_config
84
74
  eslint_result = run_eslint_command
85
75
 
86
- unused_variables = []
87
- undefined_variables = []
76
+ unused_variables = Set.new
77
+ undefined_variables = Set.new
88
78
 
89
79
  eslint_result.each do |line|
90
80
  match = REGEX_ESLINT_RESULT.match(line)
91
81
  next unless match
92
82
  if match[:type] == 'is defined but never used'
93
- unused_variables << match[:variable_name]
83
+ unused_variables.add match[:variable_name]
94
84
  else
95
- undefined_variables << match[:variable_name]
85
+ undefined_variables.add match[:variable_name]
96
86
  end
97
87
  end
98
88
 
99
- unused_variables.uniq!
100
- undefined_variables.uniq!
101
-
102
89
  old_imports = find_current_imports
103
- new_imports = old_imports[:imports].reject do |import_statement|
104
- unused_variables.each do |unused_variable|
105
- import_statement.delete_variable(unused_variable)
106
- end
107
- import_statement.empty?
108
- end
90
+ new_imports = old_imports[:imports].clone
91
+ new_imports.delete_variables!(unused_variables.to_a)
109
92
 
110
93
  undefined_variables.each do |variable|
111
94
  js_module = find_one_js_module(variable)
112
- inject_js_module(variable, js_module, new_imports) if js_module
95
+ next unless js_module
96
+ new_imports << js_module.to_import_statement(variable, @config)
97
+ end
98
+
99
+ replace_imports(old_imports[:range], new_imports)
100
+ end
101
+
102
+ def rewrite_imports
103
+ reload_config
104
+
105
+ old_imports = find_current_imports
106
+ new_imports = old_imports[:imports].clone
107
+
108
+ old_imports[:imports].each do |import|
109
+ import.variables.each do |variable|
110
+ js_module = resolve_module_using_current_imports(
111
+ find_js_modules_for(variable), variable)
112
+ next unless js_module
113
+ new_imports << js_module.to_import_statement(variable, @config)
114
+ end
113
115
  end
114
116
 
115
- replace_imports(old_imports[:newline_count],
116
- new_imports,
117
- old_imports[:imports_start_at])
117
+ replace_imports(old_imports[:range], new_imports)
118
118
  end
119
119
 
120
120
  private
@@ -158,11 +158,11 @@ module ImportJS
158
158
  stdin_data: @editor.current_file_content)
159
159
 
160
160
  if ESLINT_STDOUT_ERROR_REGEXES.any? { |regex| out =~ regex }
161
- fail ParseError.new, out
161
+ raise ParseError.new, out
162
162
  end
163
163
 
164
164
  if ESLINT_STDERR_ERROR_REGEXES.any? { |regex| err =~ regex }
165
- fail ParseError.new, err
165
+ raise ParseError.new, err
166
166
  end
167
167
 
168
168
  out.split("\n")
@@ -173,7 +173,7 @@ module ImportJS
173
173
  def find_one_js_module(variable_name)
174
174
  js_modules = []
175
175
  time do
176
- js_modules = find_js_modules(variable_name)
176
+ js_modules = find_js_modules_for(variable_name)
177
177
  end
178
178
  if js_modules.empty?
179
179
  message(
@@ -184,59 +184,30 @@ module ImportJS
184
184
  resolve_one_js_module(js_modules, variable_name)
185
185
  end
186
186
 
187
- # Add new import to the block of imports, wrapping at the max line length
188
- # @param variable_name [String]
189
- # @param js_module [ImportJS::JSModule]
190
- # @param imports [Array<ImportJS::ImportStatement>]
191
- def inject_js_module(variable_name, js_module, imports)
192
- import = imports.find do |an_import|
193
- an_import.path == js_module.import_path
194
- end
195
-
196
- if import
197
- import.declaration_keyword = @config.get(
198
- 'declaration_keyword', from_file: js_module.file_path)
199
- import.import_function = @config.get(
200
- 'import_function', from_file: js_module.file_path)
201
- if js_module.has_named_exports
202
- import.inject_named_import(variable_name)
203
- else
204
- import.set_default_import(variable_name)
205
- end
206
- else
207
- imports.unshift(js_module.to_import_statement(variable_name, @config))
208
- end
209
-
210
- # Remove duplicate import statements
211
- imports.uniq!(&:to_normalized)
212
- end
213
-
214
187
  # @param imports [Array<ImportJS::ImportStatement>]
215
188
  # @return [String]
216
189
  def generate_import_strings(import_statements)
217
190
  import_statements.map do |import|
218
- import.to_import_strings(@editor.max_line_length, @editor.tab)
191
+ import.to_import_strings(@config.get('max_line_length'),
192
+ @config.get('tab'))
219
193
  end.flatten.sort
220
194
  end
221
195
 
222
- # @param old_imports_lines [Number]
223
- # @param new_imports [Array<ImportJS::ImportStatement>]
224
- # @param imports_start_at [Number]
225
- def replace_imports(old_imports_lines, new_imports, imports_start_at)
226
- imports_end_at = old_imports_lines + imports_start_at
196
+ # @param old_imports_range [Range]
197
+ # @param new_imports [ImportJS::ImportStatements]
198
+ def replace_imports(old_imports_range, new_imports)
199
+ import_strings = new_imports.to_a
227
200
 
228
201
  # Ensure that there is a blank line after the block of all imports
229
- if old_imports_lines + new_imports.length > 0 &&
230
- !@editor.read_line(imports_end_at + 1).strip.empty?
231
- @editor.append_line(imports_end_at, '')
202
+ if old_imports_range.size + import_strings.length > 0 &&
203
+ !@editor.read_line(old_imports_range.last + 1).strip.empty?
204
+ @editor.append_line(old_imports_range.last, '')
232
205
  end
233
206
 
234
- import_strings = generate_import_strings(new_imports)
235
-
236
207
  # Find old import strings so we can compare with the new import strings
237
208
  # and see if anything has changed.
238
209
  old_import_strings = []
239
- (imports_start_at...imports_end_at).each do |line_index|
210
+ old_imports_range.each do |line_index|
240
211
  old_import_strings << @editor.read_line(line_index + 1)
241
212
  end
242
213
 
@@ -245,97 +216,72 @@ module ImportJS
245
216
  return if import_strings == old_import_strings
246
217
 
247
218
  # Delete old imports, then add the modified list back in.
248
- old_imports_lines.times { @editor.delete_line(1 + imports_start_at) }
219
+ old_imports_range.each do
220
+ @editor.delete_line(1 + old_imports_range.first)
221
+ end
249
222
  import_strings.reverse_each do |import_string|
250
223
  # We need to add each line individually because the Vim buffer will
251
224
  # convert newline characters to `~@`.
252
- import_string.split("\n").reverse_each do |line|
253
- @editor.append_line(imports_start_at, line)
225
+ if import_string.include? "\n"
226
+ import_string.split("\n").reverse_each do |line|
227
+ @editor.append_line(old_imports_range.first, line)
228
+ end
229
+ else
230
+ @editor.append_line(old_imports_range.first, import_string)
254
231
  end
255
232
  end
256
233
  end
257
234
 
258
- # @return [Number]
259
- def find_imports_start_line_index
260
- imports_start_line_index = 0
261
-
262
- # Skip over things at the top, like "use strict" and comments.
263
- inside_multi_line_comment = false
264
- matched_non_whitespace_line = false
265
- (0...@editor.count_lines).each do |line_index|
266
- line = @editor.read_line(line_index + 1)
267
-
268
- if inside_multi_line_comment || line =~ REGEX_MULTI_LINE_COMMENT_START
269
- matched_non_whitespace_line = true
270
- imports_start_line_index = line_index + 1
271
- inside_multi_line_comment = !(line =~ REGEX_MULTI_LINE_COMMENT_END)
272
- next
273
- end
274
-
275
- if line =~ REGEX_USE_STRICT || line =~ REGEX_SINGLE_LINE_COMMENT
276
- matched_non_whitespace_line = true
277
- imports_start_line_index = line_index + 1
278
- next
279
- end
280
-
281
- if line =~ REGEX_WHITESPACE_ONLY
282
- imports_start_line_index = line_index + 1
283
- next
284
- end
285
-
286
- break
287
- end
288
-
289
- # We don't want to skip over blocks that are only whitespace
290
- return imports_start_line_index if matched_non_whitespace_line
291
- 0
292
- end
235
+ REGEX_SKIP_SECTION = %r{
236
+ \s* # preceding whitespace
237
+ (?:
238
+ (['"])use\sstrict\1;? # 'use strict';
239
+ |
240
+ //.* # single-line comment
241
+ |
242
+ /\* # open multi-line comment
243
+ (?:\n|.)*? # inside of multi-line comment
244
+ \*/ # close multi-line comment
245
+ )? # ? b/c we want to match on only whitespace
246
+ \n # end of line
247
+ }x
293
248
 
294
249
  # @return [Hash]
295
250
  def find_current_imports
296
- result = {
297
- imports: [],
298
- newline_count: 0,
299
- imports_start_at: find_imports_start_line_index,
300
- }
251
+ imports_start_at = 0
252
+ newline_count = 0
301
253
 
302
- # Find block of lines that might be imports.
303
- potential_import_lines = []
304
- (result[:imports_start_at]...@editor.count_lines).each do |line_index|
305
- line = @editor.read_line(line_index + 1)
306
- break if line.strip.empty?
307
- potential_import_lines << line
254
+ scanner = StringScanner.new(@editor.current_file_content)
255
+ skipped = ''
256
+ while skip_section = scanner.scan(REGEX_SKIP_SECTION)
257
+ skipped += skip_section
308
258
  end
309
259
 
310
- # We need to put the potential imports back into a blob in order to scan
311
- # for multiline imports
312
- potential_imports_blob = potential_import_lines.join("\n")
260
+ # We don't want to skip over blocks that are only whitespace
261
+ if skipped =~ /\A(\s*\n)+\Z/m
262
+ scanner = StringScanner.new(@editor.current_file_content)
263
+ else
264
+ imports_start_at += skipped.count("\n")
265
+ end
313
266
 
314
- # Scan potential imports for everything ending in a semicolon, then
315
- # iterate through those and stop at anything that's not an import.
316
- imports = {}
317
- potential_imports_blob.scan(/^.*?;/m).each do |potential_import|
318
- import_statement = ImportStatement.parse(potential_import)
267
+ imports = ImportStatements.new(@config)
268
+ while potential_import = scanner.scan(/(^\s*\n)*^.*?;\n/m)
269
+ import_statement = ImportStatement.parse(potential_import.strip)
319
270
  break unless import_statement
320
271
 
321
- if imports[import_statement.path]
322
- # Import already exists, so this line is likely one of a named imports
323
- # pair. Combine it into the same ImportStatement.
324
- imports[import_statement.path].merge(import_statement)
325
- else
326
- # This is a new import, so we just add it to the hash.
327
- imports[import_statement.path] = import_statement
328
- end
329
-
330
- result[:newline_count] += potential_import.scan(/\n/).length + 1
272
+ imports << import_statement
273
+ newline_count += potential_import.scan(/\n/).length
331
274
  end
332
- result[:imports] = imports.values
333
- result
275
+
276
+ {
277
+ imports: imports,
278
+ range: imports_start_at...(imports_start_at + newline_count),
279
+ }
334
280
  end
335
281
 
336
282
  # @param variable_name [String]
337
283
  # @return [Array]
338
- def find_js_modules(variable_name)
284
+ def find_js_modules_for(variable_name)
339
285
  path_to_current_file = @editor.path_to_current_file
340
286
 
341
287
  alias_module = @config.resolve_alias(variable_name, path_to_current_file)
@@ -352,8 +298,8 @@ module ImportJS
352
298
  if lookup_path == ''
353
299
  # If lookup_path is an empty string, the `find` command will not work
354
300
  # as desired so we bail early.
355
- fail FindError.new,
356
- "lookup path cannot be empty (#{lookup_path.inspect})"
301
+ raise FindError.new,
302
+ "lookup path cannot be empty (#{lookup_path.inspect})"
357
303
  end
358
304
 
359
305
  find_command = %W[
@@ -364,7 +310,7 @@ module ImportJS
364
310
  command = "#{find_command} | #{egrep_command}"
365
311
  out, err = Open3.capture3(command)
366
312
 
367
- fail FindError.new, err unless err == ''
313
+ raise FindError.new, err unless err == ''
368
314
 
369
315
  matched_modules.concat(
370
316
  out.split("\n").map do |f|
@@ -443,15 +389,35 @@ module ImportJS
443
389
  # @param js_modules [Array]
444
390
  # @param variable_name [String]
445
391
  # @return [ImportJS::JSModule]
446
- def resolve_goto_module(js_modules, variable_name)
392
+ def resolve_module_using_current_imports(js_modules, variable_name)
447
393
  return js_modules.first if js_modules.length == 1
448
394
 
449
- # Look for a current import matching the goto
450
- find_current_imports[:imports].each do |ist|
395
+ # Look at the current imports and grab what is already imported for the
396
+ # variable.
397
+ matching_import_statement = find_current_imports[:imports].find do |ist|
398
+ next true if variable_name == ist.default_import
399
+ next false unless ist.named_imports
400
+ ist.named_imports.include?(variable_name)
401
+ end
402
+
403
+ if matching_import_statement
404
+ if js_modules.empty?
405
+ # We couldn't resolve any module for the variable. As a fallback, we
406
+ # can use the matching import statement. If that maps to a package
407
+ # dependency, we will still open the right file.
408
+ matched_module = JSModule.new(
409
+ import_path: matching_import_statement.path)
410
+ if matching_import_statement.named_imports?
411
+ matched_module.has_named_exports =
412
+ matching_import_statement.named_imports.include?(variable_name)
413
+ end
414
+ return matched_module
415
+ end
416
+
417
+ # Look for a module matching what is already imported
451
418
  js_modules.each do |js_module|
452
- next unless variable_name == ist.default_import ||
453
- (ist.named_imports || []).include?(variable_name)
454
- return js_module if ist.path == js_module.import_path
419
+ return js_module if matching_import_statement.path ==
420
+ js_module.import_path
455
421
  end
456
422
  end
457
423
 
@@ -16,6 +16,7 @@ module ImportJS
16
16
  # e.g. ['.js', '.jsx']
17
17
  # @param make_relative_to [String|nil] a path to a different file which the
18
18
  # resulting import path should be relative to.
19
+ # @param strip_from_path [String]
19
20
  def self.construct(lookup_path: nil,
20
21
  relative_file_path: nil,
21
22
  strip_file_extensions: nil,
@@ -30,13 +31,12 @@ module ImportJS
30
31
 
31
32
  return unless import_path
32
33
 
33
- import_path = import_path.sub(
34
- %r{^#{Regexp.escape(js_module.lookup_path)}/}, '')
34
+ import_path.sub!(%r{^#{Regexp.escape(js_module.lookup_path)}/}, '')
35
35
 
36
36
  js_module.import_path = import_path
37
37
  js_module.main_file = main_file
38
38
  js_module.make_relative_to(make_relative_to) if make_relative_to
39
- js_module.strip_from_path(strip_from_path) unless make_relative_to
39
+ js_module.strip_from_path!(strip_from_path) unless make_relative_to
40
40
  js_module
41
41
  end
42
42
 
@@ -101,15 +101,18 @@ module ImportJS
101
101
  # @param make_relative_to [String]
102
102
  def make_relative_to(make_relative_to)
103
103
  return unless lookup_path
104
+
105
+ # Prevent mutating the argument that was passed in
106
+ make_relative_to = make_relative_to.dup
107
+
104
108
  # First, strip out any absolute path up until the current directory
105
- make_relative_to = make_relative_to.sub("#{Dir.pwd}/", '')
109
+ make_relative_to.sub!("#{Dir.pwd}/", '')
106
110
 
107
111
  # Ignore if the file to relate to is part of a different lookup_path
108
112
  return unless make_relative_to.start_with? lookup_path
109
113
 
110
114
  # Strip out the lookup_path
111
- make_relative_to = make_relative_to.sub(
112
- %r{^#{Regexp.escape(lookup_path)}/}, '')
115
+ make_relative_to.sub!(%r{^#{Regexp.escape(lookup_path)}/}, '')
113
116
 
114
117
  path = Pathname.new(import_path).relative_path_from(
115
118
  Pathname.new(File.dirname(make_relative_to))
@@ -122,9 +125,9 @@ module ImportJS
122
125
  end
123
126
 
124
127
  # @param prefix [String]
125
- def strip_from_path(prefix)
128
+ def strip_from_path!(prefix)
126
129
  return unless prefix
127
- self.import_path = import_path.sub(/^#{Regexp.escape(prefix)}/, '')
130
+ import_path.sub!(/^#{Regexp.escape(prefix)}/, '')
128
131
  end
129
132
 
130
133
  # @return [String] a readable description of the module
@@ -171,18 +174,20 @@ module ImportJS
171
174
  # @param config [ImportJS::Configuration]
172
175
  # @return [ImportJS::ImportStatement]
173
176
  def to_import_statement(variable_name, config)
174
- ImportStatement.new.tap do |statement|
175
- if has_named_exports
176
- statement.inject_named_import(variable_name)
177
- else
178
- statement.default_import = variable_name
179
- end
180
- statement.path = import_path
181
- statement.declaration_keyword = config.get('declaration_keyword',
182
- from_file: file_path)
183
- statement.import_function = config.get('import_function',
184
- from_file: file_path)
177
+ if has_named_exports
178
+ named_imports = [variable_name]
179
+ else
180
+ default_import = variable_name
185
181
  end
182
+
183
+ ImportStatement.new(
184
+ declaration_keyword:
185
+ config.get('declaration_keyword', from_file: file_path),
186
+ default_import: default_import,
187
+ import_function: config.get('import_function', from_file: file_path),
188
+ named_imports: named_imports,
189
+ path: import_path
190
+ )
186
191
  end
187
192
  end
188
193
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Defines the gem version.
4
4
  module ImportJS
5
- VERSION = '0.4.1'.freeze
5
+ VERSION = '0.5.0'.freeze
6
6
  end
@@ -110,19 +110,6 @@ module ImportJS
110
110
  selected_index - 1
111
111
  end
112
112
 
113
- # Get the preferred max length of a line
114
- # @return [Number?]
115
- def max_line_length
116
- get_number('&textwidth')
117
- end
118
-
119
- # @return [String] shiftwidth number of spaces if expandtab is not set,
120
- # otherwise `\t`
121
- def tab
122
- return "\t" unless expand_tab?
123
- ' ' * (shift_width || 2)
124
- end
125
-
126
113
  private
127
114
 
128
115
  # Check for the presence of a setting such as:
@@ -147,21 +134,5 @@ module ImportJS
147
134
  def get_number(name)
148
135
  exists?(name) ? VIM.evaluate(name).to_i : nil
149
136
  end
150
-
151
- # @param name [String]
152
- # @return [Boolean?]
153
- def get_bool(name)
154
- exists?(name) ? VIM.evaluate(name).to_i != 0 : nil
155
- end
156
-
157
- # @return [Boolean?]
158
- def expand_tab?
159
- get_bool('&expandtab')
160
- end
161
-
162
- # @return [Number?]
163
- def shift_width
164
- get_number('&shiftwidth')
165
- end
166
137
  end
167
138
  end
@@ -0,0 +1,4 @@
1
+ # This is a proxy file for ./import_js.rb. Vim needs it so that we can prevent
2
+ # any installed import_js gems from being loaded before the files in the local
3
+ # filesystem.
4
+ require_relative './import_js'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: import_js
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henric Trotzig
@@ -43,10 +43,12 @@ files:
43
43
  - lib/import_js/configuration.rb
44
44
  - lib/import_js/emacs_editor.rb
45
45
  - lib/import_js/import_statement.rb
46
+ - lib/import_js/import_statements.rb
46
47
  - lib/import_js/importer.rb
47
48
  - lib/import_js/js_module.rb
48
49
  - lib/import_js/version.rb
49
50
  - lib/import_js/vim_editor.rb
51
+ - lib/vim_import_js.rb
50
52
  homepage: http://rubygems.org/gems/import_js
51
53
  licenses:
52
54
  - MIT