import_js 0.4.1 → 0.5.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 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