react-manifest-rails 0.2.14 → 0.2.16

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: 9c85309337765f3f24fae46e4cf3895e11e4a3a59b5cbfb96fa90f296f3ba48e
4
- data.tar.gz: b0f320a6ebf2a4210a4fac89d04f5609bd3d261a458f2db26fb92dd814b45465
3
+ metadata.gz: fe8b2c7c5aa432ddc840c97ed52cefc7672d5d46ee6abc07e37cf20d9f6032ab
4
+ data.tar.gz: 16448f82cee7e775111cc883820c14ff37065382452f90acd2b76abf33f807c7
5
5
  SHA512:
6
- metadata.gz: 85edbe32bf32f739097e60779a34874d1c792db5ea40ed41e671e8b0552718d4549dfddb9b120d74bcce42a15bbc598ed0384c7e609cb6f0d4044fb80900098c
7
- data.tar.gz: d2728828f1fd930cd702aa35fb4ab5ab88b4219dff1fe996fa5cdb8491888b15bb25b0fd1d44c84aa9c0707cd80b87d3d3c2554b15f0723fd979bf484db4b3f1
6
+ metadata.gz: 841fadadd182db285f3528d571bc0210178233d6da3854da67c3dc70b159110a08744e98bf52547c7f6358a3eed09d5a2ee78fed91103cdd5afa3707b0929f9b
7
+ data.tar.gz: 8352ee157c4288263bee93f86bcf01d718ef1794990f2dc76b441a9e0911b50311d12b9404eb9949f3353a2646dc350dc4a1b24e32a96b4802d6f64122dadbc1
@@ -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?
@@ -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,110 @@ 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)
87
+ ext_reqs = controller_context[:external_requires].fetch(ctrl[:bundle_name], Set.new).to_a.sort
84
88
 
85
89
  files = js_files_in(ctrl[:path])
86
- if files.empty?
90
+ own_requires = files.map { |f| relative_require_path(f) }
91
+ all_requires = (dep_requires + ext_reqs + own_requires).uniq
92
+
93
+ if all_requires.empty?
87
94
  lines << "// (no JSX files found in #{ctrl[:name]}/)"
88
95
  else
89
- files.each { |f| lines << "//= require #{relative_require_path(f)}" }
96
+ all_requires.each { |req| lines << "//= require #{req}" }
90
97
  end
91
98
 
92
99
  { filename: "#{ctrl[:bundle_name]}.js", content: "#{lines.join("\n")}\n" }
93
100
  end
94
101
 
102
+ def build_controller_context(controller_dirs)
103
+ bundle_files = {}
104
+ symbol_to_bundle = {}
105
+ external_symbol_to_require = {}
106
+ dependencies = Hash.new { |h, k| h[k] = Set.new }
107
+ external_requires = Hash.new { |h, k| h[k] = Set.new }
108
+
109
+ # Index controller-defined symbols for cross-app detection
110
+ controller_dirs.each do |ctrl|
111
+ bundle_name = ctrl[:bundle_name]
112
+ files = js_files_in(ctrl[:path])
113
+ bundle_files[bundle_name] = files
114
+
115
+ files.each do |file_path|
116
+ extract_defined_symbols(file_path).each do |sym|
117
+ next unless sym.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
118
+
119
+ symbol_to_bundle[sym] ||= bundle_name
120
+ end
121
+ end
122
+ end
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
141
+ bundle_files.each do |bundle_name, files|
142
+ files.each do |file_path|
143
+ extract_used_component_symbols(file_path).each do |sym|
144
+ dep_bundle = symbol_to_bundle[sym]
145
+ dependencies[bundle_name] << dep_bundle if dep_bundle && dep_bundle != bundle_name
146
+
147
+ req_path = external_symbol_to_require[sym]
148
+ external_requires[bundle_name] << req_path if req_path
149
+ end
150
+ end
151
+ end
152
+
153
+ {
154
+ bundle_files: bundle_files,
155
+ dependencies: dependencies,
156
+ external_requires: external_requires
157
+ }
158
+ end
159
+
160
+ def controller_dependency_requires(bundle_name, controller_context)
161
+ deps = transitive_dependencies(bundle_name, controller_context[:dependencies])
162
+ deps.flat_map { |dep_bundle| controller_context[:bundle_files].fetch(dep_bundle, []) }
163
+ .map { |abs_path| relative_require_path(abs_path) }
164
+ .uniq
165
+ .sort
166
+ end
167
+
168
+ def transitive_dependencies(bundle_name, dependency_map)
169
+ ordered = []
170
+ visiting = Set.new
171
+ visited = Set.new
172
+
173
+ walk = lambda do |current|
174
+ return if visited.include?(current) || visiting.include?(current)
175
+
176
+ visiting << current
177
+ dependency_map.fetch(current, Set.new).each { |dep| walk.call(dep) }
178
+ visiting.delete(current)
179
+
180
+ visited << current
181
+ ordered << current unless current == bundle_name
182
+ end
183
+
184
+ walk.call(bundle_name)
185
+ ordered
186
+ end
187
+
95
188
  # --------------------------------------------------------------- write
96
189
 
97
190
  def write_manifest(filename, content)
@@ -207,6 +300,54 @@ module ReactManifest
207
300
  rel.sub(/\.js\.jsx$/, "").sub(/\.jsx$/, "").sub(/\.js$/, "")
208
301
  end
209
302
 
303
+ def extract_defined_symbols(file_path)
304
+ content = File.read(file_path, encoding: "utf-8")
305
+ symbols = []
306
+ ReactManifest::Scanner::DEFINITION_PATTERNS.each do |pattern|
307
+ content.scan(pattern) { |m| symbols << m[0] }
308
+ end
309
+ symbols.uniq
310
+ rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
311
+ []
312
+ end
313
+
314
+ def extract_used_component_symbols(file_path)
315
+ content = File.read(file_path, encoding: "utf-8")
316
+
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])
329
+ end
330
+
331
+ symbols.uniq
332
+ rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
333
+ []
334
+ end
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
+
210
351
  def auto_generated?(path)
211
352
  # Avoid TOCTOU: don't check existence separately — just attempt the read
212
353
  # and treat a missing/unreadable file as not auto-generated.
@@ -238,4 +379,5 @@ module ReactManifest
238
379
  "#{counts[:skipped_pinned] || 0} skipped (not auto-generated)"
239
380
  end
240
381
  end
382
+ # rubocop:enable Metrics/ClassLength
241
383
  end
@@ -38,15 +38,13 @@ module ReactManifest
38
38
  /^export\s+class\s+([A-Z][A-Za-z0-9_]*)\s*(?:extends|\{)/ # export class Foo
39
39
  ].freeze
40
40
 
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*\(/
41
+ # Patterns to detect usage in controller files.
42
+ # Token-based patterns match any identifier occurrence regardless of syntax
43
+ # context (JSX, constructor, assignment, array, function argument, etc.).
44
+ PASCAL_TOKEN_PATTERN = /\b([A-Z][A-Za-z0-9_]*)\b/
45
+ HOOK_TOKEN_PATTERN = /\b(use[A-Z][A-Za-z0-9_]*)\b/
48
46
  # Lib calls matched against known lib symbols to reduce false positives
49
- LIB_CALL_PATTERN = /\b([a-z][A-Za-z0-9_]{2,})\s*\(/
47
+ LIB_CALL_PATTERN = /\b([a-z][A-Za-z0-9_]{2,})\s*\(/
50
48
 
51
49
  # Common JS built-ins to exclude from lib-call matching
52
50
  JS_BUILTINS = %w[
@@ -68,7 +66,7 @@ module ReactManifest
68
66
  warnings = []
69
67
  symbol_index = {}
70
68
 
71
- # Phase 1: index symbols from shared dirs
69
+ # Phase 1a: index symbols from shared dirs
72
70
  classification.shared_dirs.each do |shared_dir|
73
71
  js_files_in(shared_dir[:path]).each do |file_path|
74
72
  relative = relative_require_path(file_path)
@@ -83,6 +81,23 @@ module ReactManifest
83
81
  end
84
82
  end
85
83
 
84
+ # Phase 1b: index symbols from external_roots dirs
85
+ @config.external_roots.each do |root_path|
86
+ abs_root = abs_external_root(root_path)
87
+ js_files_in(abs_root).each do |file_path|
88
+ relative = relative_require_path(file_path)
89
+ symbols = extract_definitions(file_path)
90
+ symbols.each do |sym|
91
+ symbol_index[sym] ||= relative
92
+ end
93
+ end
94
+ end
95
+
96
+ # Phase 1c: add explicit external_providers (highest precedence — wins on conflict)
97
+ @config.external_providers.each do |sym, require_path|
98
+ symbol_index[sym] = require_path
99
+ end
100
+
86
101
  $stdout.puts "[ReactManifest] Shared symbol index: #{symbol_index.size} symbols indexed" if @config.verbose?
87
102
 
88
103
  # Phase 2: scan controller dirs for usage
@@ -190,16 +205,37 @@ module ReactManifest
190
205
  def extract_used_shared_paths(content, symbol_index)
191
206
  used = Set.new
192
207
 
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)
208
+ # Collect locally-defined symbols so we don't count a file as "using"
209
+ # its own exports (avoid self-referencing false positives).
210
+ local_syms = Set.new
211
+ DEFINITION_PATTERNS.each do |pattern|
212
+ content.scan(pattern) { |m| local_syms << m[0] }
213
+ end
214
+
215
+ # PascalCase token scan: catches JSX elements, constructors (new Foo()),
216
+ # prop values, array entries, function arguments, assignments, etc.
217
+ content.scan(PASCAL_TOKEN_PATTERN) do |match|
218
+ sym = match[0]
219
+ next if local_syms.include?(sym)
220
+ next unless symbol_index.key?(sym)
221
+
222
+ used << symbol_index[sym]
223
+ end
224
+
225
+ # Hook token scan: catches useFoo(...) and bare useFoo references.
226
+ content.scan(HOOK_TOKEN_PATTERN) do |match|
227
+ sym = match[0]
228
+ next if local_syms.include?(sym)
229
+ next unless symbol_index.key?(sym)
199
230
 
231
+ used << symbol_index[sym]
232
+ end
233
+
234
+ # Lib call scan (lowercase): already filtered to symbol_index keys.
200
235
  content.scan(LIB_CALL_PATTERN) do |match|
201
236
  sym = match[0]
202
237
  next if JS_BUILTINS.include?(sym)
238
+ next if local_syms.include?(sym)
203
239
  next unless symbol_index.key?(sym)
204
240
 
205
241
  used << symbol_index[sym]
@@ -227,5 +263,11 @@ module ReactManifest
227
263
  end
228
264
  end
229
265
  end
266
+
267
+ def abs_external_root(path)
268
+ return path if Pathname.new(path).absolute?
269
+
270
+ Rails.root.join(path).to_s
271
+ end
230
272
  end
231
273
  end
@@ -1,3 +1,3 @@
1
1
  module ReactManifest
2
- VERSION = "0.2.14".freeze
2
+ VERSION = "0.2.16".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.14
4
+ version: 0.2.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Noonan