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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03532c58c1ba02316a423460c591adf2b8caa7d6366ff89abfc5c344d1e3a8fc
4
- data.tar.gz: 3ab9a668ad0196bec652c804535307f788a7b6a9abea8418bb3c206d6f6ddba6
3
+ metadata.gz: 0076f3ffaf1c303d4ac14a8345477b3851045267b0cba8f823f49ce5f17efa4f
4
+ data.tar.gz: 579d04e786fe515e46d9c56c5c755efcf616cfd49289fcef8ccda0cc4cce7fa8
5
5
  SHA512:
6
- metadata.gz: 6ff77f4444719e08a35032a2b07bc2d6e0621d49a5db73e2b77880bbddd0172f8c3f2aea1b38b3ce40ff7feaad8d59a141fa7edabe0ce8430091f89656904a42
7
- data.tar.gz: d683a77dbcb098e2f5b08c74b1b36dac9d53e56587bf6ec7e958b5f0af25701d98fb9f70a305ceeff2ee0e2656bb81112fddf60cb61f33f21029bf6b445b8291
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
- symbol_to_bundle[sym] ||= bundle_name
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
- symbols = []
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
- symbols = []
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
- def extract_definitions(content)
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 extract_usages(content)
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