react-manifest-rails 0.2.15 → 0.2.17

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: 292c5ccc8f0c7465c5b4fc176d7b28ff5c53e11bad16689ce9ebfcf028fdc75c
4
- data.tar.gz: c77bf1b90b135fccc39b7c27ae2739c36cb891fcf504f33be2a87d3d415928d0
3
+ metadata.gz: da69c627b8ff77a1d2aa520b4b034aea63c230045ed2dccfcb0175dd9bd71e60
4
+ data.tar.gz: bbd527c3f9fe99c4e2aa972d0cb8b0ddefef6c9bd61d5022e27b5619d7a3a19b
5
5
  SHA512:
6
- metadata.gz: a7630b06c178a9d7433aaeef8eaec5c27a32319a8c6c191347cd614b1dde047ee8584cde6a2d16da4436975210479ef3cca12f62953562a6e1f49f3794829ef7
7
- data.tar.gz: 28af64d5d5424b6a86b7c8d3b6be5e5b3dd865e500561adc2e4035bd1f38fbbb98ad2211c502003f604bb976967ae8a3eeb6e06c72ca470c06bd4800cef50e8d
6
+ metadata.gz: b064cc48119471dea358e9c1f428297b518df2b6861ee5b4d0876601a7f724c4d3da97bddfc3a86555a8219d3cfb28b8269671473cb6c3c543f4a3db13741714
7
+ data.tar.gz: 62c98dbf7a8d0b15fd9cdb8ffa05090bf0929a75bd0259e5b64d84630378cd80df8176f9f49340d607a98765572ce6c5d6ec8126702f3f1324fd259aa26fcec2
@@ -54,6 +54,27 @@ module ReactManifest
54
54
  # Independent from Rails.logger output.
55
55
  attr_accessor :stdout_logging
56
56
 
57
+ # Explicit symbol-to-require-path mapping for external globals.
58
+ # Use for third-party libraries that export a PascalCase or camelCase symbol
59
+ # not located inside ux_root (e.g. MiniSearch, Chart.js wrappers).
60
+ #
61
+ # Keys are exact symbol names (case-sensitive); values are the Sprockets
62
+ # require path that should be emitted into the controller manifest.
63
+ #
64
+ # Example:
65
+ # config.external_providers = { "MiniSearch" => "mini-search", "axios" => "vendor/axios" }
66
+ attr_accessor :external_providers
67
+
68
+ # Extra root directories (absolute or relative to Rails.root) to scan for
69
+ # symbol definitions outside of ux_root. Symbols found here are added to
70
+ # the shared symbol index and will trigger an include when used by a
71
+ # controller. Empty by default; files in these dirs are subject to the
72
+ # same +exclude_paths+ and +extensions+ filters.
73
+ #
74
+ # Example:
75
+ # config.external_roots = ["app/assets/javascripts/vendor_components"]
76
+ attr_accessor :external_roots
77
+
57
78
  def initialize
58
79
  @ux_root = "app/assets/javascripts/ux"
59
80
  @app_dir = "app"
@@ -68,6 +89,8 @@ module ReactManifest
68
89
  @dry_run = false
69
90
  @verbose = false
70
91
  @stdout_logging = true
92
+ @external_providers = {}
93
+ @external_roots = []
71
94
  end
72
95
 
73
96
  def dry_run?
@@ -84,10 +84,11 @@ module ReactManifest
84
84
  def build_controller(ctrl, controller_context)
85
85
  lines = header_lines
86
86
  dep_requires = controller_dependency_requires(ctrl[:bundle_name], controller_context)
87
+ ext_reqs = controller_context[:external_requires].fetch(ctrl[:bundle_name], Set.new).to_a.sort
87
88
 
88
89
  files = js_files_in(ctrl[:path])
89
90
  own_requires = files.map { |f| relative_require_path(f) }
90
- all_requires = (dep_requires + own_requires).uniq
91
+ all_requires = (dep_requires + ext_reqs + own_requires).uniq
91
92
 
92
93
  if all_requires.empty?
93
94
  lines << "// (no JSX files found in #{ctrl[:name]}/)"
@@ -101,8 +102,11 @@ module ReactManifest
101
102
  def build_controller_context(controller_dirs)
102
103
  bundle_files = {}
103
104
  symbol_to_bundle = {}
105
+ external_symbol_to_require = {}
104
106
  dependencies = Hash.new { |h, k| h[k] = Set.new }
107
+ external_requires = Hash.new { |h, k| h[k] = Set.new }
105
108
 
109
+ # Index controller-defined symbols for cross-app detection
106
110
  controller_dirs.each do |ctrl|
107
111
  bundle_name = ctrl[:bundle_name]
108
112
  files = js_files_in(ctrl[:path])
@@ -117,20 +121,39 @@ module ReactManifest
117
121
  end
118
122
  end
119
123
 
124
+ # Index symbols from external_roots dirs
125
+ @config.external_roots.each do |root_path|
126
+ abs_root = abs_external_root(root_path)
127
+ external_js_files_in(abs_root).each do |file_path|
128
+ req_path = relative_require_path(file_path)
129
+ extract_defined_symbols(file_path).each do |sym|
130
+ external_symbol_to_require[sym] ||= req_path
131
+ end
132
+ end
133
+ end
134
+
135
+ # Explicit external_providers win over scanned roots on symbol conflicts
136
+ @config.external_providers.each do |sym, req_path|
137
+ external_symbol_to_require[sym] = req_path
138
+ end
139
+
140
+ # Compute per-bundle cross-app and external dependencies
120
141
  bundle_files.each do |bundle_name, files|
121
142
  files.each do |file_path|
122
143
  extract_used_component_symbols(file_path).each do |sym|
123
144
  dep_bundle = symbol_to_bundle[sym]
124
- next unless dep_bundle && dep_bundle != bundle_name
145
+ dependencies[bundle_name] << dep_bundle if dep_bundle && dep_bundle != bundle_name
125
146
 
126
- dependencies[bundle_name] << dep_bundle
147
+ req_path = external_symbol_to_require[sym]
148
+ external_requires[bundle_name] << req_path if req_path
127
149
  end
128
150
  end
129
151
  end
130
152
 
131
153
  {
132
154
  bundle_files: bundle_files,
133
- dependencies: dependencies
155
+ dependencies: dependencies,
156
+ external_requires: external_requires
134
157
  }
135
158
  end
136
159
 
@@ -290,14 +313,19 @@ module ReactManifest
290
313
 
291
314
  def extract_used_component_symbols(file_path)
292
315
  content = File.read(file_path, encoding: "utf-8")
293
- symbols = []
294
316
 
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 }
317
+ # Collect locally-defined symbols to avoid self-reference false positives
318
+ local_syms = Set.new
319
+ ReactManifest::Scanner::DEFINITION_PATTERNS.each do |pattern|
320
+ content.scan(pattern) { |m| local_syms << m[0] }
321
+ end
322
+
323
+ symbols = []
324
+ content.scan(ReactManifest::Scanner::PASCAL_TOKEN_PATTERN) do |m|
325
+ symbols << m[0] unless local_syms.include?(m[0])
326
+ end
327
+ content.scan(ReactManifest::Scanner::HOOK_TOKEN_PATTERN) do |m|
328
+ symbols << m[0] unless local_syms.include?(m[0])
301
329
  end
302
330
 
303
331
  symbols.uniq
@@ -305,6 +333,21 @@ module ReactManifest
305
333
  []
306
334
  end
307
335
 
336
+ def external_js_files_in(dir)
337
+ return [] unless Dir.exist?(dir)
338
+
339
+ Dir.glob(File.join(dir, "**", @config.extensions_glob))
340
+ .reject { |f| File.directory?(f) }
341
+ .reject { |f| excluded_path?(f) }
342
+ .sort
343
+ end
344
+
345
+ def abs_external_root(path)
346
+ return path if Pathname.new(path).absolute?
347
+
348
+ Rails.root.join(path).to_s
349
+ end
350
+
308
351
  def auto_generated?(path)
309
352
  # Avoid TOCTOU: don't check existence separately — just attempt the read
310
353
  # and treat a missing/unreadable file as not auto-generated.
@@ -15,6 +15,7 @@ module ReactManifest
15
15
  # and produces per-controller lists of referenced shared files.
16
16
  #
17
17
  # Phase 3 — emits non-fatal warnings.
18
+ # rubocop:disable Metrics/ClassLength
18
19
  class Scanner
19
20
  # Patterns to detect symbol definitions (CommonJS and ES module style)
20
21
  DEFINITION_PATTERNS = [
@@ -38,15 +39,13 @@ module ReactManifest
38
39
  /^export\s+class\s+([A-Z][A-Za-z0-9_]*)\s*(?:extends|\{)/ # export class Foo
39
40
  ].freeze
40
41
 
41
- # Patterns to detect usage in controller files
42
- JSX_ELEMENT_PATTERN = %r{<([A-Z][A-Za-z0-9_]*)[\s/>]}
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*\]/
47
- HOOK_CALL_PATTERN = /\b(use[A-Z][A-Za-z0-9_]*)\s*\(/
42
+ # Patterns to detect usage in controller files.
43
+ # Token-based patterns match any identifier occurrence regardless of syntax
44
+ # context (JSX, constructor, assignment, array, function argument, etc.).
45
+ PASCAL_TOKEN_PATTERN = /\b([A-Z][A-Za-z0-9_]*)\b/
46
+ HOOK_TOKEN_PATTERN = /\b(use[A-Z][A-Za-z0-9_]*)\b/
48
47
  # Lib calls matched against known lib symbols to reduce false positives
49
- LIB_CALL_PATTERN = /\b([a-z][A-Za-z0-9_]{2,})\s*\(/
48
+ LIB_CALL_PATTERN = /\b([a-z][A-Za-z0-9_]{2,})\s*\(/
50
49
 
51
50
  # Common JS built-ins to exclude from lib-call matching
52
51
  JS_BUILTINS = %w[
@@ -58,21 +57,24 @@ module ReactManifest
58
57
  Object Array String Number Boolean Symbol Map Set WeakMap
59
58
  ].freeze
60
59
 
61
- Result = Struct.new(:symbol_index, :controller_usages, :warnings, keyword_init: true)
60
+ Result = Struct.new(:symbol_index, :controller_usages, :warnings, :shared_violations, keyword_init: true)
62
61
 
63
62
  def initialize(config = ReactManifest.configuration)
64
63
  @config = config
65
64
  end
66
65
 
66
+ # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity
67
67
  def scan(classification)
68
68
  warnings = []
69
69
  symbol_index = {}
70
70
 
71
- # Phase 1: index symbols from shared dirs
71
+ # Phase 1a: index symbols from shared dirs
72
+ shared_file_paths = {} # file_path => relative_require_path for all shared files
72
73
  classification.shared_dirs.each do |shared_dir|
73
74
  js_files_in(shared_dir[:path]).each do |file_path|
74
75
  relative = relative_require_path(file_path)
75
- symbols = extract_definitions(file_path)
76
+ shared_file_paths[file_path] = relative
77
+ symbols = extract_definitions(file_path)
76
78
  symbols.each do |sym|
77
79
  if symbol_index.key?(sym)
78
80
  warnings << "Duplicate symbol '#{sym}' in #{relative} (already from #{symbol_index[sym]})"
@@ -83,8 +85,41 @@ module ReactManifest
83
85
  end
84
86
  end
85
87
 
88
+ # Phase 1b: index symbols from external_roots dirs
89
+ @config.external_roots.each do |root_path|
90
+ abs_root = abs_external_root(root_path)
91
+ js_files_in(abs_root).each do |file_path|
92
+ relative = relative_require_path(file_path)
93
+ symbols = extract_definitions(file_path)
94
+ symbols.each do |sym|
95
+ symbol_index[sym] ||= relative
96
+ end
97
+ end
98
+ end
99
+
100
+ # Phase 1c: add explicit external_providers (highest precedence — wins on conflict)
101
+ @config.external_providers.each do |sym, require_path|
102
+ symbol_index[sym] = require_path
103
+ end
104
+
105
+ # Phase 1d: build controller (app-dir) symbol index for violation detection
106
+ controller_symbol_index = {}
107
+ classification.controller_dirs.each do |ctrl|
108
+ js_files_in(ctrl[:path]).each do |file_path|
109
+ extract_definitions(file_path).each do |sym|
110
+ controller_symbol_index[sym] ||= {
111
+ file: relative_require_path(file_path),
112
+ controller: ctrl[:name]
113
+ }
114
+ end
115
+ end
116
+ end
117
+
86
118
  $stdout.puts "[ReactManifest] Shared symbol index: #{symbol_index.size} symbols indexed" if @config.verbose?
87
119
 
120
+ # Phase 1e: detect shared files that use app-dir (controller) symbols
121
+ shared_violations = detect_shared_violations(shared_file_paths, controller_symbol_index, warnings)
122
+
88
123
  # Phase 2: scan controller dirs for usage
89
124
  controller_usages = {}
90
125
 
@@ -111,9 +146,11 @@ module ReactManifest
111
146
  Result.new(
112
147
  symbol_index: symbol_index,
113
148
  controller_usages: controller_usages,
114
- warnings: warnings
149
+ warnings: warnings,
150
+ shared_violations: shared_violations
115
151
  )
116
152
  end
153
+ # rubocop:enable Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity
117
154
 
118
155
  private
119
156
 
@@ -162,8 +199,38 @@ module ReactManifest
162
199
  "'#{ctrl_name}_<action>.js.jsx' naming convention"
163
200
  end
164
201
 
202
+ def detect_shared_violations(shared_file_paths, controller_symbol_index, warnings)
203
+ violations = []
204
+ shared_file_paths.each do |file_path, relative|
205
+ content = begin
206
+ File.read(file_path, encoding: "utf-8")
207
+ rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
208
+ next
209
+ end
210
+
211
+ local_syms = Set.new
212
+ DEFINITION_PATTERNS.each { |p| content.scan(p) { |m| local_syms << m[0] } }
213
+
214
+ [PASCAL_TOKEN_PATTERN, HOOK_TOKEN_PATTERN].each do |pattern|
215
+ content.scan(pattern) do |match|
216
+ sym = match[0]
217
+ next if local_syms.include?(sym)
218
+ next unless controller_symbol_index.key?(sym)
219
+
220
+ info = controller_symbol_index[sym]
221
+ violations << { shared_file: relative, symbol: sym,
222
+ controller: info[:controller], app_file: info[:file] }
223
+ warnings << "Shared file '#{relative}' uses app-dir symbol '#{sym}' " \
224
+ "(from ux/app/#{info[:controller]}). " \
225
+ "Move '#{sym}' to a shared dir or the shared file will be incomplete."
226
+ end
227
+ end
228
+ end
229
+ violations
230
+ end
231
+
232
+ # Count how many controllers use each shared file
165
233
  def emit_fanout_warnings(controller_usages, warnings)
166
- # Count how many controllers use each shared file
167
234
  fanout = Hash.new(0)
168
235
  controller_usages.each_value do |files|
169
236
  files.each { |f| fanout[f] += 1 }
@@ -190,16 +257,37 @@ module ReactManifest
190
257
  def extract_used_shared_paths(content, symbol_index)
191
258
  used = Set.new
192
259
 
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)
260
+ # Collect locally-defined symbols so we don't count a file as "using"
261
+ # its own exports (avoid self-referencing false positives).
262
+ local_syms = Set.new
263
+ DEFINITION_PATTERNS.each do |pattern|
264
+ content.scan(pattern) { |m| local_syms << m[0] }
265
+ end
266
+
267
+ # PascalCase token scan: catches JSX elements, constructors (new Foo()),
268
+ # prop values, array entries, function arguments, assignments, etc.
269
+ content.scan(PASCAL_TOKEN_PATTERN) do |match|
270
+ sym = match[0]
271
+ next if local_syms.include?(sym)
272
+ next unless symbol_index.key?(sym)
273
+
274
+ used << symbol_index[sym]
275
+ end
276
+
277
+ # Hook token scan: catches useFoo(...) and bare useFoo references.
278
+ content.scan(HOOK_TOKEN_PATTERN) do |match|
279
+ sym = match[0]
280
+ next if local_syms.include?(sym)
281
+ next unless symbol_index.key?(sym)
282
+
283
+ used << symbol_index[sym]
284
+ end
199
285
 
286
+ # Lib call scan (lowercase): already filtered to symbol_index keys.
200
287
  content.scan(LIB_CALL_PATTERN) do |match|
201
288
  sym = match[0]
202
289
  next if JS_BUILTINS.include?(sym)
290
+ next if local_syms.include?(sym)
203
291
  next unless symbol_index.key?(sym)
204
292
 
205
293
  used << symbol_index[sym]
@@ -227,5 +315,12 @@ module ReactManifest
227
315
  end
228
316
  end
229
317
  end
318
+
319
+ def abs_external_root(path)
320
+ return path if Pathname.new(path).absolute?
321
+
322
+ Rails.root.join(path).to_s
323
+ end
230
324
  end
325
+ # rubocop:enable Metrics/ClassLength
231
326
  end
@@ -1,3 +1,3 @@
1
1
  module ReactManifest
2
- VERSION = "0.2.15".freeze
2
+ VERSION = "0.2.17".freeze
3
3
  end
@@ -165,14 +165,19 @@ module ReactManifest
165
165
 
166
166
  def extract_used_component_symbols(file_path)
167
167
  content = File.read(file_path, encoding: "utf-8")
168
- symbols = []
169
168
 
170
- content.scan(ReactManifest::Scanner::JSX_ELEMENT_PATTERN) { |m| symbols << m[0] }
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 }
169
+ # Collect locally-defined symbols to avoid self-reference false positives
170
+ local_syms = Set.new
171
+ ReactManifest::Scanner::DEFINITION_PATTERNS.each do |pattern|
172
+ content.scan(pattern) { |m| local_syms << m[0] }
173
+ end
174
+
175
+ symbols = []
176
+ content.scan(ReactManifest::Scanner::PASCAL_TOKEN_PATTERN) do |m|
177
+ symbols << m[0] unless local_syms.include?(m[0])
178
+ end
179
+ content.scan(ReactManifest::Scanner::HOOK_TOKEN_PATTERN) do |m|
180
+ symbols << m[0] unless local_syms.include?(m[0])
176
181
  end
177
182
 
178
183
  symbols.uniq
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.15
4
+ version: 0.2.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Noonan