react-manifest-rails 0.2.33 → 0.3.0

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: 42ca130740decc2c19123ab8e2d0b368925f0bd988736d84a9087d4016e1836c
4
+ data.tar.gz: e9e23a7c0eb8dea12f4eb8498f08bff0ea11fe56f168833ae3ef13e090b4d59a
5
5
  SHA512:
6
- metadata.gz: 6ff77f4444719e08a35032a2b07bc2d6e0621d49a5db73e2b77880bbddd0172f8c3f2aea1b38b3ce40ff7feaad8d59a141fa7edabe0ce8430091f89656904a42
7
- data.tar.gz: d683a77dbcb098e2f5b08c74b1b36dac9d53e56587bf6ec7e958b5f0af25701d98fb9f70a305ceeff2ee0e2656bb81112fddf60cb61f33f21029bf6b445b8291
6
+ metadata.gz: d73c047a2257f229e26682658c2644c0645fd362ba30f2f2ef34fceb15ecf83cf282b2c7899472e3eb25351b30512b5a3e8dbe456e1057502453876f2ce1868d
7
+ data.tar.gz: 78ebfd5b3390fa6204d6a59a46d94f1aef87c87359b499049f745da68d493f2dab3dd4fefe1f46667ca1be40f66c02e17fa1b977f3fc5b67370a9ba37a9b0566
data/CHANGELOG.md CHANGED
@@ -7,6 +7,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-07-15
11
+
12
+ ### Fixed
13
+ - `always_include` no longer double-declares. Its bundles are delivered on every page
14
+ by their own `<script>` tag (both `react_bundle_tag`/`resolve_bundles` and
15
+ `react_component` now emit them), and their files are no longer inlined into each
16
+ controller manifest. Previously the separate tag plus the inline loaded an
17
+ always_include bundle's **private** files twice on any other controller's page, and a
18
+ **collided** symbol whose definer lives in an always_include bundle could pull a
19
+ second definer into the consumer — both causing "Identifier X has already been
20
+ declared". Controller dependency resolution now treats symbols provided by an
21
+ always_include bundle as already satisfied, and skips those bundles when inlining
22
+ dependencies (legacy `auto_shared = false` path included). The "already provided"
23
+ set is computed from the exact files the always_include `<script>` tag actually
24
+ carries (its own non-promoted files plus the canonical dependency files it inlines),
25
+ not from whole dependency bundles — so a consumer never drops a definer for a symbol
26
+ that lives in a sibling file the tag does not load. Symbol availability that the
27
+ previous inlining protected (added in 0.2.24) is preserved by the tag `react_component`
28
+ now emits.
29
+ - One case remains inherently unresolvable by dependency arithmetic: when an
30
+ `always_include` bundle *consumes* a collided symbol, its tag inlines the
31
+ first-writer definer, which also lives in that definer's own bundle manifest, so on
32
+ the definer's own page the file loads twice. `react_manifest:generate` now emits a
33
+ warning naming the always_include bundle, the duplicated file/symbol, and the owning
34
+ bundle, with the fix (rename or relocate the collided symbol). Detected in both the
35
+ default and legacy (`auto_shared = false`) modes.
36
+
37
+ ### Added
38
+ - Auto-promotion: a component defined under `ux/app/<controller>/` but used by any
39
+ other bundle is now emitted into `ux_shared` (loaded once per page) instead of being
40
+ inlined into each consumer. This eliminates duplicate top-level `const` declarations
41
+ ("Identifier X has already been declared") — and the cascading `X is not defined`
42
+ errors that follow when a SyntaxError aborts a concatenated bundle — for components
43
+ shared across controllers (e.g. a navbar component pulled in via `always_include`).
44
+ Files never move; only the `//= require` line's destination changes. Promotion is
45
+ transitive. `react_manifest:analyze` now lists what was promoted and why. Set
46
+ `config.auto_shared = false` to restore the previous inline behavior.
47
+
48
+ ### Changed
49
+ - Generated `ux_manifests/*.js` are now treated as build artifacts. `react_manifest:setup`
50
+ and the development boot step add them to `.gitignore` (idempotently) and keep a
51
+ committed `.keep` so the directory always exists; setup prints a one-time
52
+ `git rm --cached` to untrack previously-committed manifests. Production is unaffected
53
+ (manifests regenerate during `assets:precompile`). Opt out with
54
+ `config.manage_gitignore = false`. Upgrading an existing app requires no manual step:
55
+ the first dev boot regenerates manifests and ensures the ignore entry.
56
+
57
+ ## [0.2.34] - 2026-07-02
58
+
59
+ ### Added
60
+ - 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`.
61
+ - 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.
62
+
10
63
  ## [0.2.33] - 2026-07-01
11
64
 
12
65
  ### 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
@@ -21,9 +21,18 @@ module ReactManifest
21
21
  # Keeping generated files out of output_dir root avoids clutter.
22
22
  attr_accessor :manifest_subdir
23
23
 
24
+ # When true (default), the gem ensures the generated manifest dir is gitignored
25
+ # (adds the entry on dev boot if missing). Set false to manage .gitignore yourself.
26
+ attr_accessor :manage_gitignore
27
+
24
28
  # Bundle name for auto-generated shared bundle (all non-app/ dirs)
25
29
  attr_accessor :shared_bundle
26
30
 
31
+ # When true (default), a component defined under app_dir but used by any other
32
+ # bundle is emitted into the shared bundle (loaded once per page) instead of
33
+ # being inlined into each consumer. Set false to restore legacy inlining.
34
+ attr_accessor :auto_shared
35
+
27
36
  # Bundles always prepended by react_bundle_tag (e.g. ["ux_main"])
28
37
  attr_accessor :always_include
29
38
 
@@ -31,6 +40,22 @@ module ReactManifest
31
40
  # Example: ignore = ["admin"] skips ux/app/admin/* when building ux_<controller>.js.
32
41
  attr_accessor :ignore
33
42
 
43
+ # Controller directory names (under ux_root/app_dir) whose files must never
44
+ # be inlined into any OTHER controller's manifest via inferred cross-app
45
+ # dependency detection. Their own ux_<name>.js manifest is unaffected —
46
+ # still includes all of their own files as usual.
47
+ #
48
+ # Symbol usage detection is pure regex (no AST), so a generic component
49
+ # name can collide with an unrelated word appearing as plain JSX text
50
+ # elsewhere (e.g. a "Show" component vs a "Show More" button label in a
51
+ # completely different controller) and get wrongly inlined everywhere.
52
+ # Use this for self-contained dirs (e.g. a page-builder tool bundling its
53
+ # own vendored library) that should truly never leak into other bundles.
54
+ #
55
+ # Example:
56
+ # config.isolated_app_dirs = ["rvb"]
57
+ attr_accessor :isolated_app_dirs
58
+
34
59
  # Path segments to exclude while scanning files under ux_root.
35
60
  # This is segment matching (not full path matching), so "vendor" excludes
36
61
  # ux/vendor/foo.js and ux/app/users/vendor/bar.jsx, but not ux/vendor_custom/x.js.
@@ -85,9 +110,12 @@ module ReactManifest
85
110
  @app_dir = "app"
86
111
  @output_dir = "app/assets/javascripts"
87
112
  @manifest_subdir = "ux_manifests"
113
+ @manage_gitignore = true
88
114
  @shared_bundle = "ux_shared"
115
+ @auto_shared = true
89
116
  @always_include = []
90
117
  @ignore = []
118
+ @isolated_app_dirs = []
91
119
  @exclude_paths = %w[react react_dev vendor]
92
120
  @size_threshold_kb = 500
93
121
  @extensions = %w[js jsx]
@@ -102,6 +130,10 @@ module ReactManifest
102
130
  !!@dry_run
103
131
  end
104
132
 
133
+ def auto_shared?
134
+ !!@auto_shared
135
+ end
136
+
105
137
  def verbose?
106
138
  !!@verbose
107
139
  end
@@ -110,6 +142,10 @@ module ReactManifest
110
142
  !!@stdout_logging
111
143
  end
112
144
 
145
+ def manage_gitignore?
146
+ !!@manage_gitignore
147
+ end
148
+
113
149
  # Glob fragment used by Dir.glob, e.g. "*.{js,jsx}" or "*.{js,jsx,ts,tsx}"
114
150
  def extensions_glob
115
151
  "*.{#{extensions.join(',')}}"
@@ -150,7 +186,7 @@ module ReactManifest
150
186
  end
151
187
 
152
188
  def cache_key
153
- [ux_root, app_dir, extensions, always_include, exclude_paths, external_providers].hash
189
+ [ux_root, app_dir, extensions, always_include, exclude_paths, external_providers, isolated_app_dirs].hash
154
190
  end
155
191
  end
156
192
  end