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 +4 -4
- data/lib/react_manifest/configuration.rb +23 -0
- data/lib/react_manifest/generator.rb +54 -11
- data/lib/react_manifest/scanner.rb +114 -19
- 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: da69c627b8ff77a1d2aa520b4b034aea63c230045ed2dccfcb0175dd9bd71e60
|
|
4
|
+
data.tar.gz: bbd527c3f9fe99c4e2aa972d0cb8b0ddefef6c9bd61d5022e27b5619d7a3a19b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
145
|
+
dependencies[bundle_name] << dep_bundle if dep_bundle && dep_bundle != bundle_name
|
|
125
146
|
|
|
126
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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*\(/
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
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
|