react-manifest-rails 0.2.13 → 0.2.15

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
  SHA256:
3
- metadata.gz: 78103a797d9951bb67d7e9cd4fcbb0fbf5a6589cd5995f52f745de079b3d6824
4
- data.tar.gz: 229244d4844f0accdc1c23538f0a5ec8965a35e5246a62abc91dfae3ce16676d
3
+ metadata.gz: 292c5ccc8f0c7465c5b4fc176d7b28ff5c53e11bad16689ce9ebfcf028fdc75c
4
+ data.tar.gz: c77bf1b90b135fccc39b7c27ae2739c36cb891fcf504f33be2a87d3d415928d0
5
5
  SHA512:
6
- metadata.gz: 43ba51a2daca72844e5d6606fa4e6c15dda1cef4b42582ab2db4d2a49037675157814a20030e41f60d9bc6c395dd2681f548f0244884b733104f37b66996c8a2
7
- data.tar.gz: 01a71eda1dc257b0e0c151e34ac227323a2349c02c8d7709edd619975ba9051c9f759f89b440e317e6575d1e37edfba0c9da20ed2f306d57b587ddd239162ca1
6
+ metadata.gz: a7630b06c178a9d7433aaeef8eaec5c27a32319a8c6c191347cd614b1dde047ee8584cde6a2d16da4436975210479ef3cca12f62953562a6e1f49f3794829ef7
7
+ data.tar.gz: 28af64d5d5424b6a86b7c8d3b6be5e5b3dd865e500561adc2e4035bd1f38fbbb98ad2211c502003f604bb976967ae8a3eeb6e06c72ca470c06bd4800cef50e8d
@@ -22,6 +22,7 @@ module ReactManifest
22
22
  # to avoid partial reads from concurrent processes.
23
23
  #
24
24
  # Never touches application.js, application_dev.js, or files in exclude_paths.
25
+ # rubocop:disable Metrics/ClassLength
25
26
  class Generator
26
27
  HEADER = <<~JS.freeze
27
28
  // AUTO-GENERATED — DO NOT EDIT
@@ -41,11 +42,12 @@ module ReactManifest
41
42
  # written and others stale/missing.
42
43
  def run!
43
44
  classification = @classifier.classify
45
+ controller_context = build_controller_context(classification.controller_dirs)
44
46
 
45
47
  # Phase 1: build all content in memory — no I/O.
46
48
  manifests = []
47
49
  manifests << build_shared(classification.shared_dirs)
48
- classification.controller_dirs.each { |ctrl| manifests << build_controller(ctrl) }
50
+ classification.controller_dirs.each { |ctrl| manifests << build_controller(ctrl, controller_context) }
49
51
 
50
52
  migrate_legacy_manifests!
51
53
 
@@ -79,19 +81,87 @@ module ReactManifest
79
81
 
80
82
  # --------------------------------------------------------------- controller
81
83
 
82
- def build_controller(ctrl)
84
+ def build_controller(ctrl, controller_context)
83
85
  lines = header_lines
86
+ dep_requires = controller_dependency_requires(ctrl[:bundle_name], controller_context)
84
87
 
85
88
  files = js_files_in(ctrl[:path])
86
- if files.empty?
89
+ own_requires = files.map { |f| relative_require_path(f) }
90
+ all_requires = (dep_requires + own_requires).uniq
91
+
92
+ if all_requires.empty?
87
93
  lines << "// (no JSX files found in #{ctrl[:name]}/)"
88
94
  else
89
- files.each { |f| lines << "//= require #{relative_require_path(f)}" }
95
+ all_requires.each { |req| lines << "//= require #{req}" }
90
96
  end
91
97
 
92
98
  { filename: "#{ctrl[:bundle_name]}.js", content: "#{lines.join("\n")}\n" }
93
99
  end
94
100
 
101
+ def build_controller_context(controller_dirs)
102
+ bundle_files = {}
103
+ symbol_to_bundle = {}
104
+ dependencies = Hash.new { |h, k| h[k] = Set.new }
105
+
106
+ controller_dirs.each do |ctrl|
107
+ bundle_name = ctrl[:bundle_name]
108
+ files = js_files_in(ctrl[:path])
109
+ bundle_files[bundle_name] = files
110
+
111
+ files.each do |file_path|
112
+ extract_defined_symbols(file_path).each do |sym|
113
+ next unless sym.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
114
+
115
+ symbol_to_bundle[sym] ||= bundle_name
116
+ end
117
+ end
118
+ end
119
+
120
+ bundle_files.each do |bundle_name, files|
121
+ files.each do |file_path|
122
+ extract_used_component_symbols(file_path).each do |sym|
123
+ dep_bundle = symbol_to_bundle[sym]
124
+ next unless dep_bundle && dep_bundle != bundle_name
125
+
126
+ dependencies[bundle_name] << dep_bundle
127
+ end
128
+ end
129
+ end
130
+
131
+ {
132
+ bundle_files: bundle_files,
133
+ dependencies: dependencies
134
+ }
135
+ end
136
+
137
+ def controller_dependency_requires(bundle_name, controller_context)
138
+ deps = transitive_dependencies(bundle_name, controller_context[:dependencies])
139
+ deps.flat_map { |dep_bundle| controller_context[:bundle_files].fetch(dep_bundle, []) }
140
+ .map { |abs_path| relative_require_path(abs_path) }
141
+ .uniq
142
+ .sort
143
+ end
144
+
145
+ def transitive_dependencies(bundle_name, dependency_map)
146
+ ordered = []
147
+ visiting = Set.new
148
+ visited = Set.new
149
+
150
+ walk = lambda do |current|
151
+ return if visited.include?(current) || visiting.include?(current)
152
+
153
+ visiting << current
154
+ dependency_map.fetch(current, Set.new).each { |dep| walk.call(dep) }
155
+ visiting.delete(current)
156
+
157
+ visited << current
158
+ ordered << current unless current == bundle_name
159
+ end
160
+
161
+ walk.call(bundle_name)
162
+ ordered
163
+ end
164
+
95
165
  # --------------------------------------------------------------- write
96
166
 
97
167
  def write_manifest(filename, content)
@@ -207,6 +277,34 @@ module ReactManifest
207
277
  rel.sub(/\.js\.jsx$/, "").sub(/\.jsx$/, "").sub(/\.js$/, "")
208
278
  end
209
279
 
280
+ def extract_defined_symbols(file_path)
281
+ content = File.read(file_path, encoding: "utf-8")
282
+ symbols = []
283
+ ReactManifest::Scanner::DEFINITION_PATTERNS.each do |pattern|
284
+ content.scan(pattern) { |m| symbols << m[0] }
285
+ end
286
+ symbols.uniq
287
+ rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
288
+ []
289
+ end
290
+
291
+ def extract_used_component_symbols(file_path)
292
+ content = File.read(file_path, encoding: "utf-8")
293
+ symbols = []
294
+
295
+ content.scan(ReactManifest::Scanner::JSX_ELEMENT_PATTERN) { |m| symbols << m[0] }
296
+ content.scan(ReactManifest::Scanner::REACT_CREATE_PATTERN) { |m| symbols << m[0] }
297
+ content.scan(ReactManifest::Scanner::JSX_PROP_COMPONENT_PATTERN) { |m| symbols << m[0] }
298
+ content.scan(ReactManifest::Scanner::OBJECT_COMPONENT_PATTERN) { |m| symbols << m[0] }
299
+ content.scan(ReactManifest::Scanner::ARRAY_COMPONENT_LIST_PATTERN) do |m|
300
+ m[0].split(/\s*,\s*/).each { |sym| symbols << sym }
301
+ end
302
+
303
+ symbols.uniq
304
+ rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
305
+ []
306
+ end
307
+
210
308
  def auto_generated?(path)
211
309
  # Avoid TOCTOU: don't check existence separately — just attempt the read
212
310
  # and treat a missing/unreadable file as not auto-generated.
@@ -238,4 +336,5 @@ module ReactManifest
238
336
  "#{counts[:skipped_pinned] || 0} skipped (not auto-generated)"
239
337
  end
240
338
  end
339
+ # rubocop:enable Metrics/ClassLength
241
340
  end
@@ -41,6 +41,9 @@ module ReactManifest
41
41
  # Patterns to detect usage in controller files
42
42
  JSX_ELEMENT_PATTERN = %r{<([A-Z][A-Za-z0-9_]*)[\s/>]}
43
43
  REACT_CREATE_PATTERN = /React\.createElement\(\s*([A-Z][A-Za-z0-9_]*)[\s,)]/
44
+ JSX_PROP_COMPONENT_PATTERN = /[A-Za-z_][A-Za-z0-9_]*\s*=\s*\{\s*([A-Z][A-Za-z0-9_]*)\s*\}/
45
+ OBJECT_COMPONENT_PATTERN = /:\s*([A-Z][A-Za-z0-9_]*)\b/
46
+ ARRAY_COMPONENT_LIST_PATTERN = /\[\s*([A-Z][A-Za-z0-9_]*(?:\s*,\s*[A-Z][A-Za-z0-9_]*)*\s*,?)\s*\]/
44
47
  HOOK_CALL_PATTERN = /\b(use[A-Z][A-Za-z0-9_]*)\s*\(/
45
48
  # Lib calls matched against known lib symbols to reduce false positives
46
49
  LIB_CALL_PATTERN = /\b([a-z][A-Za-z0-9_]{2,})\s*\(/
@@ -93,41 +96,10 @@ module ReactManifest
93
96
 
94
97
  files.each do |file_path|
95
98
  validate_naming(file_path, ctrl[:name], warnings)
96
- begin
97
- content = File.read(file_path, encoding: "utf-8")
98
- rescue Errno::ENOENT, Errno::EACCES => e
99
- warnings << "Skipping #{file_path}: #{e.message}"
100
- next
101
- rescue Encoding::InvalidByteSequenceError
102
- warnings << "Skipping #{file_path}: not valid UTF-8"
103
- next
104
- end
105
-
106
- # JSX element usage: <PrimaryButton (JSX tag syntax)
107
- content.scan(JSX_ELEMENT_PATTERN) do |match|
108
- sym = match[0]
109
- used << symbol_index[sym] if symbol_index.key?(sym)
110
- end
111
-
112
- # React.createElement(PrimaryButton, ...) (non-JSX style)
113
- content.scan(REACT_CREATE_PATTERN) do |match|
114
- sym = match[0]
115
- used << symbol_index[sym] if symbol_index.key?(sym)
116
- end
117
-
118
- # Hook calls: useFetch(
119
- content.scan(HOOK_CALL_PATTERN) do |match|
120
- sym = match[0]
121
- used << symbol_index[sym] if symbol_index.key?(sym)
122
- end
99
+ content = read_controller_file(file_path, warnings)
100
+ next unless content
123
101
 
124
- # Lib function calls: formatDate( — filtered against lib symbol index
125
- content.scan(LIB_CALL_PATTERN) do |match|
126
- sym = match[0]
127
- next if JS_BUILTINS.include?(sym)
128
-
129
- used << symbol_index[sym] if symbol_index.key?(sym)
130
- end
102
+ used.merge(extract_used_shared_paths(content, symbol_index))
131
103
  end
132
104
 
133
105
  controller_usages[ctrl[:name]] = used.to_a.sort
@@ -204,5 +176,56 @@ module ReactManifest
204
176
  end
205
177
  end
206
178
  end
179
+
180
+ def read_controller_file(file_path, warnings)
181
+ File.read(file_path, encoding: "utf-8")
182
+ rescue Errno::ENOENT, Errno::EACCES => e
183
+ warnings << "Skipping #{file_path}: #{e.message}"
184
+ nil
185
+ rescue Encoding::InvalidByteSequenceError
186
+ warnings << "Skipping #{file_path}: not valid UTF-8"
187
+ nil
188
+ end
189
+
190
+ def extract_used_shared_paths(content, symbol_index)
191
+ used = Set.new
192
+
193
+ scan_component_usage(content, JSX_ELEMENT_PATTERN, symbol_index, used)
194
+ scan_component_usage(content, REACT_CREATE_PATTERN, symbol_index, used)
195
+ scan_component_usage(content, JSX_PROP_COMPONENT_PATTERN, symbol_index, used)
196
+ scan_component_usage(content, OBJECT_COMPONENT_PATTERN, symbol_index, used)
197
+ scan_array_component_usage(content, symbol_index, used)
198
+ scan_component_usage(content, HOOK_CALL_PATTERN, symbol_index, used)
199
+
200
+ content.scan(LIB_CALL_PATTERN) do |match|
201
+ sym = match[0]
202
+ next if JS_BUILTINS.include?(sym)
203
+ next unless symbol_index.key?(sym)
204
+
205
+ used << symbol_index[sym]
206
+ end
207
+
208
+ used
209
+ end
210
+
211
+ def scan_component_usage(content, pattern, symbol_index, used)
212
+ content.scan(pattern) do |match|
213
+ sym = match[0]
214
+ next unless symbol_index.key?(sym)
215
+
216
+ used << symbol_index[sym]
217
+ end
218
+ end
219
+
220
+ def scan_array_component_usage(content, symbol_index, used)
221
+ content.scan(ARRAY_COMPONENT_LIST_PATTERN) do |match|
222
+ list = match[0]
223
+ list.split(/\s*,\s*/).each do |sym|
224
+ next unless symbol_index.key?(sym)
225
+
226
+ used << symbol_index[sym]
227
+ end
228
+ end
229
+ end
207
230
  end
208
231
  end
@@ -1,3 +1,3 @@
1
1
  module ReactManifest
2
- VERSION = "0.2.13".freeze
2
+ VERSION = "0.2.15".freeze
3
3
  end
@@ -169,6 +169,11 @@ module ReactManifest
169
169
 
170
170
  content.scan(ReactManifest::Scanner::JSX_ELEMENT_PATTERN) { |m| symbols << m[0] }
171
171
  content.scan(ReactManifest::Scanner::REACT_CREATE_PATTERN) { |m| symbols << m[0] }
172
+ content.scan(ReactManifest::Scanner::JSX_PROP_COMPONENT_PATTERN) { |m| symbols << m[0] }
173
+ content.scan(ReactManifest::Scanner::OBJECT_COMPONENT_PATTERN) { |m| symbols << m[0] }
174
+ content.scan(ReactManifest::Scanner::ARRAY_COMPONENT_LIST_PATTERN) do |m|
175
+ m[0].split(/\s*,\s*/).each { |sym| symbols << sym }
176
+ end
172
177
 
173
178
  symbols.uniq
174
179
  rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: react-manifest-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.13
4
+ version: 0.2.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Noonan