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 +4 -4
- data/lib/react_manifest/configuration.rb +23 -0
- data/lib/react_manifest/generator.rb +146 -4
- data/lib/react_manifest/scanner.rb +57 -15
- data/lib/react_manifest/version.rb +1 -1
- data/lib/react_manifest.rb +12 -7
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fe8b2c7c5aa432ddc840c97ed52cefc7672d5d46ee6abc07e37cf20d9f6032ab
|
|
4
|
+
data.tar.gz: 16448f82cee7e775111cc883820c14ff37065382452f90acd2b76abf33f807c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
data/lib/react_manifest.rb
CHANGED
|
@@ -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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|