react-manifest-rails 0.2.33 → 0.2.34
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/CHANGELOG.md +6 -0
- data/README.md +3 -0
- data/lib/react_manifest/ast_extractor.rb +70 -0
- data/lib/react_manifest/configuration.rb +18 -1
- data/lib/react_manifest/generator.rb +10 -7
- data/lib/react_manifest/scanner.rb +6 -10
- data/lib/react_manifest/symbol_extractor.rb +37 -4
- data/lib/react_manifest/vendor/ast_extractor.js +19 -0
- data/lib/react_manifest/vendor/build/README.md +19 -0
- data/lib/react_manifest/vendor/build/entry.js +137 -0
- data/lib/react_manifest/vendor/build/package-lock.json +524 -0
- data/lib/react_manifest/vendor/build/package.json +11 -0
- data/lib/react_manifest/version.rb +1 -1
- data/lib/react_manifest.rb +6 -3
- metadata +21 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0076f3ffaf1c303d4ac14a8345477b3851045267b0cba8f823f49ce5f17efa4f
|
|
4
|
+
data.tar.gz: 579d04e786fe515e46d9c56c5c755efcf616cfd49289fcef8ccda0cc4cce7fa8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d221314836bc176da1378824e2ab7a55232d5977db2ca46b353d98f499b8e49d31b7999d0787953944db9c8b1c8652f3f17422918ee59d0fd5b78f5d46dca65
|
|
7
|
+
data.tar.gz: 3eeaba00d80ad2e7c73b1320026ea4b3ed0ed989b5a3c33513a2e45f6e63f7b632b466c2cb192e163b42892f4bce76b2dc59b87b90d90869c4eaa5137f4a8a86
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.34] - 2026-07-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Optional AST-based symbol extraction: add `gem "mini_racer"` to opt in. Definitions and usages are derived from a real JS/JSX AST (Acorn + acorn-jsx) instead of regex when available, fixing false positives where an unrelated word (e.g. plain JSX text like "Show More") is indistinguishable from a real component reference to a regex scanner. Falls back to regex extraction per-file on any parse failure, always with a line-numbered warning. No behavior change for apps that don't add `mini_racer`.
|
|
14
|
+
- New `config.isolated_app_dirs` option. A listed controller dir's own manifest is generated as usual, but its files/symbols are never inferred as a dependency of any *other* controller's manifest. Without the AST extraction above (i.e. no `mini_racer`), symbol usage detection is regex-based, so a generic component name can collide with an unrelated word appearing as plain JSX text elsewhere (e.g. a `Show` component vs. a "Show More" button label in a completely different controller) and get wrongly inlined into every other bundle. Use this as a manual, always-available fallback for self-contained dirs — e.g. a page-builder tool bundling its own vendored library — that should never leak into other pages, whether or not `mini_racer` is installed.
|
|
15
|
+
|
|
10
16
|
## [0.2.33] - 2026-07-01
|
|
11
17
|
|
|
12
18
|
### Fixed
|
data/README.md
CHANGED
|
@@ -131,6 +131,7 @@ ReactManifest.configure do |config|
|
|
|
131
131
|
config.extensions = %w[js jsx] # add ts tsx for TypeScript
|
|
132
132
|
config.always_include = [] # extra shared files always added to every bundle
|
|
133
133
|
config.ignore = [] # controller dir names to skip entirely
|
|
134
|
+
config.isolated_app_dirs = [] # controller dir names that must never leak into other bundles
|
|
134
135
|
config.exclude_paths = ["react", "react_dev", "vendor"] # path segments to exclude
|
|
135
136
|
config.size_threshold_kb = 500 # warn if a bundle exceeds this
|
|
136
137
|
config.dry_run = false # never write; only print what would change
|
|
@@ -142,11 +143,13 @@ end
|
|
|
142
143
|
### Key option notes
|
|
143
144
|
|
|
144
145
|
- **`ignore`**: skips entire controller dirs under `ux/app/`. `ignore = ["admin"]` excludes `ux/app/admin/`.
|
|
146
|
+
- **`isolated_app_dirs`**: a controller dir's own `ux_<name>.js` is generated as usual, but its files/symbols are never inferred as a dependency of any *other* controller. Without the optional AST engine below (i.e. no `mini_racer`), symbol usage detection is regex-based, so a generic component name can collide with an unrelated word appearing as plain JSX text elsewhere (e.g. a `Show` component vs. a "Show More" button label in a different controller) and get wrongly inlined everywhere. Use this as a manual, always-available fallback for self-contained dirs — e.g. a page-builder tool bundling its own vendored library — that should never leak into other pages, whether or not `mini_racer` is installed. `isolated_app_dirs = ["rvb"]` isolates `ux/app/rvb/`.
|
|
145
147
|
- **`exclude_paths`**: excludes files whose path contains any listed segment. Not based on `application.js`.
|
|
146
148
|
- **`dry_run`**: also honoured by `DRY_RUN=1` environment variable at runtime.
|
|
147
149
|
- **`extensions`**: add `ts` and `tsx` to enable TypeScript source detection.
|
|
148
150
|
- **`external_roots` / `external_providers`**: do not point these at files already inside shared `ux/` dirs (`components/`, `hooks/`, `lib/`, etc.). The generator now skips those overlaps to prevent duplicate declarations in browser runtime.
|
|
149
151
|
- **`stdout_logging`**: `Rails.logger` always receives status lines regardless of this flag. Turn this on only if your `Rails.logger` does *not* already print to your terminal in development — if it does (common with `RAILS_LOG_TO_STDOUT`, Docker, or `bin/dev`/Foreman setups), enabling this prints every line twice.
|
|
152
|
+
- **AST-based symbol extraction**: add `gem "mini_racer"` to your Gemfile to opt in. When present, definitions and usages are derived from a real JS/JSX AST (Acorn) instead of regex, which fixes false positives like a `Show` component colliding with unrelated text such as a "Show More" button label. Falls back to regex extraction per-file on any parse failure (always logged via `Rails.logger.warn` with file/line/column when available) — generation never breaks because of this. Without `mini_racer`, behavior is unchanged from today.
|
|
150
153
|
|
|
151
154
|
## Commands
|
|
152
155
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require "mini_racer"
|
|
2
|
+
require "json"
|
|
3
|
+
require_relative "logging"
|
|
4
|
+
|
|
5
|
+
module ReactManifest
|
|
6
|
+
# AST-based symbol extraction backed by an embedded V8 (mini_racer).
|
|
7
|
+
# Mirrors SymbolExtractor's public interface exactly. Never raises — any
|
|
8
|
+
# parse or runtime failure returns nil (after logging a warning) so the
|
|
9
|
+
# caller (SymbolExtractor's dispatcher) can fall back to regex extraction
|
|
10
|
+
# for that one file.
|
|
11
|
+
module AstExtractor
|
|
12
|
+
VENDOR_SCRIPT = File.expand_path("vendor/ast_extractor.js", __dir__)
|
|
13
|
+
|
|
14
|
+
GLUE = <<~JS.freeze
|
|
15
|
+
function __reactManifestExtractDefinitions(content) {
|
|
16
|
+
return JSON.stringify(__astExtractor.extractDefinitions(content));
|
|
17
|
+
}
|
|
18
|
+
function __reactManifestExtractUsages(content) {
|
|
19
|
+
return JSON.stringify(__astExtractor.extractUsages(content));
|
|
20
|
+
}
|
|
21
|
+
JS
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
include ReactManifest::Logging
|
|
25
|
+
|
|
26
|
+
def extract_definitions(content, file_path: nil)
|
|
27
|
+
result = call_js("__reactManifestExtractDefinitions", content)
|
|
28
|
+
return result["definitions"] if result["success"]
|
|
29
|
+
|
|
30
|
+
report_error(file_path, result["error"])
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def extract_usages(content, file_path: nil)
|
|
35
|
+
result = call_js("__reactManifestExtractUsages", content)
|
|
36
|
+
return result["usages"] if result["success"]
|
|
37
|
+
|
|
38
|
+
report_error(file_path, result["error"])
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def context
|
|
45
|
+
@context ||= begin
|
|
46
|
+
ctx = MiniRacer::Context.new
|
|
47
|
+
ctx.eval(File.read(VENDOR_SCRIPT))
|
|
48
|
+
ctx.eval(GLUE)
|
|
49
|
+
ctx
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def call_js(function_name, content)
|
|
54
|
+
JSON.parse(context.call(function_name, content))
|
|
55
|
+
rescue MiniRacer::Error => e
|
|
56
|
+
@context = nil
|
|
57
|
+
{ "success" => false, "error" => { "message" => e.message, "line" => nil, "column" => nil } }
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
{ "success" => false, "error" => { "message" => e.message, "line" => nil, "column" => nil } }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def report_error(file_path, error)
|
|
63
|
+
location = file_path || "unknown file"
|
|
64
|
+
location = "#{location} at line #{error['line']}, column #{error['column']}" if error && error["line"]
|
|
65
|
+
message = error ? error["message"] : "unknown error"
|
|
66
|
+
log_warn "AST parse failed for #{location}: #{message} — falling back to regex extraction for this file."
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -31,6 +31,22 @@ module ReactManifest
|
|
|
31
31
|
# Example: ignore = ["admin"] skips ux/app/admin/* when building ux_<controller>.js.
|
|
32
32
|
attr_accessor :ignore
|
|
33
33
|
|
|
34
|
+
# Controller directory names (under ux_root/app_dir) whose files must never
|
|
35
|
+
# be inlined into any OTHER controller's manifest via inferred cross-app
|
|
36
|
+
# dependency detection. Their own ux_<name>.js manifest is unaffected —
|
|
37
|
+
# still includes all of their own files as usual.
|
|
38
|
+
#
|
|
39
|
+
# Symbol usage detection is pure regex (no AST), so a generic component
|
|
40
|
+
# name can collide with an unrelated word appearing as plain JSX text
|
|
41
|
+
# elsewhere (e.g. a "Show" component vs a "Show More" button label in a
|
|
42
|
+
# completely different controller) and get wrongly inlined everywhere.
|
|
43
|
+
# Use this for self-contained dirs (e.g. a page-builder tool bundling its
|
|
44
|
+
# own vendored library) that should truly never leak into other bundles.
|
|
45
|
+
#
|
|
46
|
+
# Example:
|
|
47
|
+
# config.isolated_app_dirs = ["rvb"]
|
|
48
|
+
attr_accessor :isolated_app_dirs
|
|
49
|
+
|
|
34
50
|
# Path segments to exclude while scanning files under ux_root.
|
|
35
51
|
# This is segment matching (not full path matching), so "vendor" excludes
|
|
36
52
|
# ux/vendor/foo.js and ux/app/users/vendor/bar.jsx, but not ux/vendor_custom/x.js.
|
|
@@ -88,6 +104,7 @@ module ReactManifest
|
|
|
88
104
|
@shared_bundle = "ux_shared"
|
|
89
105
|
@always_include = []
|
|
90
106
|
@ignore = []
|
|
107
|
+
@isolated_app_dirs = []
|
|
91
108
|
@exclude_paths = %w[react react_dev vendor]
|
|
92
109
|
@size_threshold_kb = 500
|
|
93
110
|
@extensions = %w[js jsx]
|
|
@@ -150,7 +167,7 @@ module ReactManifest
|
|
|
150
167
|
end
|
|
151
168
|
|
|
152
169
|
def cache_key
|
|
153
|
-
[ux_root, app_dir, extensions, always_include, exclude_paths, external_providers].hash
|
|
170
|
+
[ux_root, app_dir, extensions, always_include, exclude_paths, external_providers, isolated_app_dirs].hash
|
|
154
171
|
end
|
|
155
172
|
end
|
|
156
173
|
end
|
|
@@ -150,11 +150,18 @@ module ReactManifest
|
|
|
150
150
|
files = js_files_in(ctrl[:path])
|
|
151
151
|
bundle_files[bundle_name] = files
|
|
152
152
|
|
|
153
|
+
isolated = @config.isolated_app_dirs.include?(ctrl[:name])
|
|
154
|
+
|
|
153
155
|
files.each do |file_path|
|
|
154
156
|
extract_defined_symbols(file_path).each do |sym|
|
|
155
157
|
next unless sym.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
|
|
156
158
|
|
|
157
|
-
|
|
159
|
+
# Isolated dirs never register into the shared symbol index, so
|
|
160
|
+
# no other bundle can ever be inferred to depend on them —
|
|
161
|
+
# regardless of what their own symbol names happen to collide
|
|
162
|
+
# with (regex-based usage detection can't tell a real component
|
|
163
|
+
# reference from an unrelated word appearing as plain JSX text).
|
|
164
|
+
symbol_to_bundle[sym] ||= bundle_name unless isolated
|
|
158
165
|
bundle_own_symbols[bundle_name] << sym
|
|
159
166
|
end
|
|
160
167
|
end
|
|
@@ -388,18 +395,14 @@ module ReactManifest
|
|
|
388
395
|
|
|
389
396
|
def extract_defined_symbols(file_path)
|
|
390
397
|
content = File.read(file_path, encoding: "utf-8")
|
|
391
|
-
|
|
392
|
-
ReactManifest::Scanner::DEFINITION_PATTERNS.each do |pattern|
|
|
393
|
-
content.scan(pattern) { |m| symbols << m[0] }
|
|
394
|
-
end
|
|
395
|
-
symbols.uniq
|
|
398
|
+
SymbolExtractor.extract_definitions(content, file_path: file_path)
|
|
396
399
|
rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
|
|
397
400
|
[]
|
|
398
401
|
end
|
|
399
402
|
|
|
400
403
|
def extract_used_component_symbols(file_path)
|
|
401
404
|
content = File.read(file_path, encoding: "utf-8")
|
|
402
|
-
SymbolExtractor.extract_usages(content)
|
|
405
|
+
SymbolExtractor.extract_usages(content, file_path: file_path)
|
|
403
406
|
rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
|
|
404
407
|
[]
|
|
405
408
|
end
|
|
@@ -166,7 +166,7 @@ module ReactManifest
|
|
|
166
166
|
cache = self.class.file_symbol_cache
|
|
167
167
|
return cache[file_path] if cache.key?(file_path)
|
|
168
168
|
|
|
169
|
-
cache[file_path] = parse_definitions(content)
|
|
169
|
+
cache[file_path] = parse_definitions(content, file_path: file_path)
|
|
170
170
|
end
|
|
171
171
|
|
|
172
172
|
def scan_file_definitions(file_path)
|
|
@@ -175,17 +175,13 @@ module ReactManifest
|
|
|
175
175
|
rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
|
|
176
176
|
return []
|
|
177
177
|
end
|
|
178
|
-
parse_definitions(content)
|
|
178
|
+
parse_definitions(content, file_path: file_path)
|
|
179
179
|
end
|
|
180
180
|
|
|
181
|
-
def parse_definitions(content)
|
|
181
|
+
def parse_definitions(content, file_path: nil)
|
|
182
182
|
return [] unless content
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
DEFINITION_PATTERNS.each do |pattern|
|
|
186
|
-
content.scan(pattern) { |m| symbols << m[0] }
|
|
187
|
-
end
|
|
188
|
-
symbols.uniq
|
|
184
|
+
SymbolExtractor.extract_definitions(content, file_path: file_path)
|
|
189
185
|
end
|
|
190
186
|
|
|
191
187
|
def relative_require_path(abs_path)
|
|
@@ -201,7 +197,7 @@ module ReactManifest
|
|
|
201
197
|
content = shared_file_content[file_path]
|
|
202
198
|
next unless content
|
|
203
199
|
|
|
204
|
-
SymbolExtractor.extract_usages(content).each do |sym|
|
|
200
|
+
SymbolExtractor.extract_usages(content, file_path: file_path).each do |sym|
|
|
205
201
|
next unless controller_symbol_index.key?(sym)
|
|
206
202
|
|
|
207
203
|
info = controller_symbol_index[sym]
|
|
@@ -224,7 +220,7 @@ module ReactManifest
|
|
|
224
220
|
next
|
|
225
221
|
end
|
|
226
222
|
|
|
227
|
-
SymbolExtractor.extract_usages(content).each do |sym|
|
|
223
|
+
SymbolExtractor.extract_usages(content, file_path: file_path).each do |sym|
|
|
228
224
|
next unless controller_symbol_index.key?(sym)
|
|
229
225
|
|
|
230
226
|
info = controller_symbol_index[sym]
|
|
@@ -30,9 +30,44 @@ module ReactManifest
|
|
|
30
30
|
|
|
31
31
|
module_function
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
# Uses the AST engine (mini_racer) when available; falls back to regex
|
|
34
|
+
# extraction for this content on any AST failure (unavailable engine,
|
|
35
|
+
# parse error, unsupported syntax such as TypeScript).
|
|
36
|
+
def extract_definitions(content, file_path: nil)
|
|
34
37
|
return [] unless content
|
|
35
38
|
|
|
39
|
+
if ast_engine_available?
|
|
40
|
+
result = AstExtractor.extract_definitions(content, file_path: file_path)
|
|
41
|
+
return result if result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
regex_extract_definitions(content)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def extract_usages(content, file_path: nil)
|
|
48
|
+
return [] unless content
|
|
49
|
+
|
|
50
|
+
if ast_engine_available?
|
|
51
|
+
result = AstExtractor.extract_usages(content, file_path: file_path)
|
|
52
|
+
return result if result
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
regex_extract_usages(content)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Decided once per process: does `require "mini_racer"` succeed?
|
|
59
|
+
def ast_engine_available?
|
|
60
|
+
return @ast_engine_available if defined?(@ast_engine_available)
|
|
61
|
+
|
|
62
|
+
@ast_engine_available = begin
|
|
63
|
+
require_relative "ast_extractor"
|
|
64
|
+
true
|
|
65
|
+
rescue LoadError
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def regex_extract_definitions(content)
|
|
36
71
|
symbols = []
|
|
37
72
|
DEFINITION_PATTERNS.each do |pattern|
|
|
38
73
|
content.scan(pattern) { |m| symbols << m[0] }
|
|
@@ -40,9 +75,7 @@ module ReactManifest
|
|
|
40
75
|
symbols.uniq
|
|
41
76
|
end
|
|
42
77
|
|
|
43
|
-
def
|
|
44
|
-
return [] unless content
|
|
45
|
-
|
|
78
|
+
def regex_extract_usages(content)
|
|
46
79
|
local_syms = Set.new
|
|
47
80
|
DEFINITION_PATTERNS.each { |p| content.scan(p) { |m| local_syms << m[0] } }
|
|
48
81
|
|