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 +4 -4
- data/lib/react_manifest/generator.rb +103 -4
- data/lib/react_manifest/scanner.rb +57 -34
- data/lib/react_manifest/version.rb +1 -1
- data/lib/react_manifest.rb +5 -0
- 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: 292c5ccc8f0c7465c5b4fc176d7b28ff5c53e11bad16689ce9ebfcf028fdc75c
|
|
4
|
+
data.tar.gz: c77bf1b90b135fccc39b7c27ae2739c36cb891fcf504f33be2a87d3d415928d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
data/lib/react_manifest.rb
CHANGED
|
@@ -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
|