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 +4 -4
- data/CHANGELOG.md +53 -0
- data/README.md +3 -0
- data/lib/react_manifest/ast_extractor.rb +70 -0
- data/lib/react_manifest/configuration.rb +37 -1
- data/lib/react_manifest/generator.rb +362 -63
- data/lib/react_manifest/gitignore_patcher.rb +75 -0
- data/lib/react_manifest/railtie.rb +6 -0
- 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 +35 -7
- data/tasks/react_manifest.rake +24 -0
- metadata +22 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 42ca130740decc2c19123ab8e2d0b368925f0bd988736d84a9087d4016e1836c
|
|
4
|
+
data.tar.gz: e9e23a7c0eb8dea12f4eb8498f08bff0ea11fe56f168833ae3ef13e090b4d59a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|