react-manifest-rails 0.2.34 → 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 +47 -0
- data/lib/react_manifest/configuration.rb +19 -0
- data/lib/react_manifest/generator.rb +359 -63
- data/lib/react_manifest/gitignore_patcher.rb +75 -0
- data/lib/react_manifest/railtie.rb +6 -0
- data/lib/react_manifest/version.rb +1 -1
- data/lib/react_manifest.rb +29 -4
- data/tasks/react_manifest.rake +24 -0
- metadata +2 -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,53 @@ 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
|
+
|
|
10
57
|
## [0.2.34] - 2026-07-02
|
|
11
58
|
|
|
12
59
|
### Added
|
|
@@ -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
|
|
|
@@ -101,7 +110,9 @@ module ReactManifest
|
|
|
101
110
|
@app_dir = "app"
|
|
102
111
|
@output_dir = "app/assets/javascripts"
|
|
103
112
|
@manifest_subdir = "ux_manifests"
|
|
113
|
+
@manage_gitignore = true
|
|
104
114
|
@shared_bundle = "ux_shared"
|
|
115
|
+
@auto_shared = true
|
|
105
116
|
@always_include = []
|
|
106
117
|
@ignore = []
|
|
107
118
|
@isolated_app_dirs = []
|
|
@@ -119,6 +130,10 @@ module ReactManifest
|
|
|
119
130
|
!!@dry_run
|
|
120
131
|
end
|
|
121
132
|
|
|
133
|
+
def auto_shared?
|
|
134
|
+
!!@auto_shared
|
|
135
|
+
end
|
|
136
|
+
|
|
122
137
|
def verbose?
|
|
123
138
|
!!@verbose
|
|
124
139
|
end
|
|
@@ -127,6 +142,10 @@ module ReactManifest
|
|
|
127
142
|
!!@stdout_logging
|
|
128
143
|
end
|
|
129
144
|
|
|
145
|
+
def manage_gitignore?
|
|
146
|
+
!!@manage_gitignore
|
|
147
|
+
end
|
|
148
|
+
|
|
130
149
|
# Glob fragment used by Dir.glob, e.g. "*.{js,jsx}" or "*.{js,jsx,ts,tsx}"
|
|
131
150
|
def extensions_glob
|
|
132
151
|
"*.{#{extensions.join(',')}}"
|
|
@@ -42,6 +42,14 @@ module ReactManifest
|
|
|
42
42
|
@classifier = TreeClassifier.new(config)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# After a run (or on demand), maps a promoted file's require path to the set of
|
|
46
|
+
# bundle names that forced its promotion. Used by react_manifest:analyze.
|
|
47
|
+
def promotion_reasons
|
|
48
|
+
classification = @classifier.classify
|
|
49
|
+
build_controller_context(classification.controller_dirs, classification.shared_dirs)
|
|
50
|
+
@promotion_reasons || {}
|
|
51
|
+
end
|
|
52
|
+
|
|
45
53
|
# Run full generation. Returns array of {path:, status:} hashes.
|
|
46
54
|
#
|
|
47
55
|
# All manifest content is built first (no filesystem writes), then written
|
|
@@ -49,10 +57,10 @@ module ReactManifest
|
|
|
49
57
|
# written and others stale/missing.
|
|
50
58
|
def run!
|
|
51
59
|
classification = @classifier.classify
|
|
52
|
-
controller_context = build_controller_context(classification.controller_dirs)
|
|
60
|
+
controller_context = build_controller_context(classification.controller_dirs, classification.shared_dirs)
|
|
53
61
|
|
|
54
62
|
# Phase 1: build all content in memory — no I/O.
|
|
55
|
-
shared_manifest = build_shared(classification.shared_dirs)
|
|
63
|
+
shared_manifest = build_shared(classification.shared_dirs, controller_context[:promoted_files])
|
|
56
64
|
manifests = [shared_manifest] + classification.controller_dirs.map do |ctrl|
|
|
57
65
|
build_controller(ctrl, controller_context)
|
|
58
66
|
end
|
|
@@ -98,10 +106,9 @@ module ReactManifest
|
|
|
98
106
|
|
|
99
107
|
# ------------------------------------------------------------------ shared
|
|
100
108
|
|
|
101
|
-
def build_shared(shared_dirs)
|
|
109
|
+
def build_shared(shared_dirs, promoted_files = Set.new)
|
|
102
110
|
lines = header_lines
|
|
103
|
-
reqs = shared_dirs
|
|
104
|
-
.flat_map { |d| js_files_in(d[:path]) }
|
|
111
|
+
reqs = (shared_dirs.flat_map { |d| js_files_in(d[:path]) } + promoted_files.to_a)
|
|
105
112
|
.map { |f| normalize_require_path(relative_require_path(f)) }
|
|
106
113
|
.uniq
|
|
107
114
|
.sort
|
|
@@ -119,13 +126,21 @@ module ReactManifest
|
|
|
119
126
|
|
|
120
127
|
def build_controller(ctrl, controller_context)
|
|
121
128
|
lines = header_lines
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
promoted = controller_context[:promoted_files]
|
|
130
|
+
bundle_name = ctrl[:bundle_name]
|
|
131
|
+
# always_include bundles are NOT inlined here: resolve_bundles /
|
|
132
|
+
# react_component deliver them on every page via their own <script> tag,
|
|
133
|
+
# so inlining would load the same file twice and double-declare.
|
|
134
|
+
dep_requires = if @config.auto_shared?
|
|
135
|
+
promoted_aware_dependency_requires(bundle_name, controller_context)
|
|
136
|
+
else
|
|
137
|
+
controller_dependency_requires(bundle_name, controller_context)
|
|
138
|
+
end
|
|
139
|
+
ext_reqs = controller_context[:external_requires].fetch(bundle_name, Set.new).to_a.sort
|
|
140
|
+
|
|
141
|
+
files = js_files_in(ctrl[:path]).reject { |f| promoted.include?(f) }
|
|
127
142
|
own_requires = files.map { |f| relative_require_path(f) }
|
|
128
|
-
all_requires = (
|
|
143
|
+
all_requires = (dep_requires + ext_reqs + own_requires).uniq
|
|
129
144
|
|
|
130
145
|
if all_requires.empty?
|
|
131
146
|
lines << "// (no JSX files found in #{ctrl[:name]}/)"
|
|
@@ -136,66 +151,303 @@ module ReactManifest
|
|
|
136
151
|
{ filename: "#{ctrl[:bundle_name]}.js", content: "#{lines.join("\n")}\n" }
|
|
137
152
|
end
|
|
138
153
|
|
|
139
|
-
def build_controller_context(controller_dirs)
|
|
154
|
+
def build_controller_context(controller_dirs, shared_dirs)
|
|
155
|
+
bundle_files, file_owner, file_defs, symbol_to_bundle, symbol_to_file,
|
|
156
|
+
symbol_definers, bundle_own_symbols = index_controller_symbols(controller_dirs)
|
|
157
|
+
|
|
158
|
+
file_uses = Hash.new { |h, k| h[k] = Set.new }
|
|
159
|
+
symbol_used_by_bundles = Hash.new { |h, k| h[k] = Set.new }
|
|
160
|
+
record_symbol_usages(bundle_files, shared_dirs, file_uses, symbol_used_by_bundles)
|
|
161
|
+
|
|
162
|
+
collided = symbol_definers.each_with_object(Set.new) do |(sym, files), acc|
|
|
163
|
+
acc << sym if files.size >= 2
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
external_symbol_to_require = index_external_symbols(symbol_to_bundle)
|
|
167
|
+
|
|
168
|
+
dependencies, external_requires = build_dependency_graph(
|
|
169
|
+
bundle_files, bundle_own_symbols, file_uses, symbol_to_bundle, external_symbol_to_require
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
symbol_index = {
|
|
173
|
+
file_defs: file_defs,
|
|
174
|
+
symbol_to_file: symbol_to_file,
|
|
175
|
+
symbol_used_by_bundles: symbol_used_by_bundles
|
|
176
|
+
}
|
|
177
|
+
promoted_files =
|
|
178
|
+
if @config.auto_shared?
|
|
179
|
+
compute_promoted_files(file_owner, file_uses, symbol_index, collided)
|
|
180
|
+
else
|
|
181
|
+
Set.new
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
context = {
|
|
185
|
+
bundle_files: bundle_files,
|
|
186
|
+
dependencies: dependencies,
|
|
187
|
+
external_requires: external_requires,
|
|
188
|
+
promoted_files: promoted_files,
|
|
189
|
+
file_uses: file_uses,
|
|
190
|
+
symbol_to_file: symbol_to_file,
|
|
191
|
+
bundle_own_symbols: bundle_own_symbols
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# Bundles guaranteed loaded on every page by always_include (used to exclude
|
|
195
|
+
# them from the legacy whole-bundle inline path and to self-exclude an
|
|
196
|
+
# always_include bundle's own manifest from the skip below).
|
|
197
|
+
context[:always_loaded_bundles] = compute_always_loaded_bundles(dependencies)
|
|
198
|
+
# Symbols actually delivered on every page by the always_include <script>
|
|
199
|
+
# tag(s) — derived from the real files those tags carry, NOT from whole dep
|
|
200
|
+
# bundles (which would over-claim symbols the tag does not load and cause a
|
|
201
|
+
# consumer to skip a definer it still needs). Unused by the legacy path.
|
|
202
|
+
context[:always_provided_symbols] =
|
|
203
|
+
@config.auto_shared? ? compute_always_provided_symbols(context, file_defs) : Set.new
|
|
204
|
+
|
|
205
|
+
warn_on_always_include_collisions(context, file_owner, file_defs, collided)
|
|
206
|
+
|
|
207
|
+
context
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Surface the one case dependency arithmetic cannot resolve: an always_include
|
|
211
|
+
# bundle that CONSUMES a collided symbol. Its tag inlines the first-writer
|
|
212
|
+
# definer file, which also lives in that file's own bundle manifest — so on the
|
|
213
|
+
# definer's own page the file loads twice (once via the always_include tag,
|
|
214
|
+
# once via the owning bundle), a duplicate declaration. We cannot auto-fix it
|
|
215
|
+
# (there is no collision-free page-global home for a collided symbol), so we
|
|
216
|
+
# warn with the actionable fix: rename or relocate the symbol.
|
|
217
|
+
def warn_on_always_include_collisions(context, file_owner, file_defs, collided)
|
|
218
|
+
return if collided.empty?
|
|
219
|
+
|
|
220
|
+
bundles = @config.always_include.map(&:to_s).reject(&:empty?).uniq
|
|
221
|
+
bundles.each do |always_bundle|
|
|
222
|
+
always_include_foreign_files(always_bundle, context).each do |file|
|
|
223
|
+
owner = file_owner[file]
|
|
224
|
+
next if owner.nil? || owner == always_bundle
|
|
225
|
+
|
|
226
|
+
collided_syms = (file_defs.fetch(file, Set.new) & collided).to_a.sort
|
|
227
|
+
next if collided_syms.empty?
|
|
228
|
+
|
|
229
|
+
plural = collided_syms.size > 1 ? "s" : ""
|
|
230
|
+
log_warn "always_include bundle '#{always_bundle}' pulls in " \
|
|
231
|
+
"'#{relative_require_path(file)}' (defines collided symbol#{plural} " \
|
|
232
|
+
"#{collided_syms.join(', ')}) owned by '#{owner}'. On #{owner}'s own page " \
|
|
233
|
+
"that file loads twice — once via the #{always_bundle} tag and once via " \
|
|
234
|
+
"#{owner} — causing a duplicate declaration. Rename the symbol or move it " \
|
|
235
|
+
"to a uniquely-named shared component."
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Files an always_include bundle's tag delivers from OTHER bundles: under
|
|
241
|
+
# auto_shared, the exact non-promoted dependency files it inlines; in legacy
|
|
242
|
+
# mode, every file of the dependency bundles it inlines wholesale.
|
|
243
|
+
def always_include_foreign_files(always_bundle, context)
|
|
244
|
+
if @config.auto_shared?
|
|
245
|
+
promoted_aware_dependency_files(always_bundle, context, Set.new)
|
|
246
|
+
else
|
|
247
|
+
transitive_dependencies(always_bundle, context[:dependencies])
|
|
248
|
+
.flat_map { |dep| context[:bundle_files].fetch(dep, []) }
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Symbols the always_include tag(s) put on every page: for each always_include
|
|
253
|
+
# bundle, the symbols defined by its own non-promoted files plus the canonical
|
|
254
|
+
# dependency files it inlines (its manifest's exact file closure).
|
|
255
|
+
def compute_always_provided_symbols(context, file_defs)
|
|
256
|
+
bundles = @config.always_include.map(&:to_s).reject(&:empty?).uniq
|
|
257
|
+
return Set.new if bundles.empty?
|
|
258
|
+
|
|
259
|
+
always_files = Set.new
|
|
260
|
+
bundles.each do |b|
|
|
261
|
+
context[:bundle_files].fetch(b, []).each do |file|
|
|
262
|
+
always_files << file unless context[:promoted_files].include?(file)
|
|
263
|
+
end
|
|
264
|
+
promoted_aware_dependency_files(b, context, Set.new).each { |file| always_files << file }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
always_files.each_with_object(Set.new) { |file, syms| syms.merge(file_defs.fetch(file, Set.new)) }
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Files that can never be promoted to ux_shared: any file defining a "collided"
|
|
271
|
+
# symbol (a name with 2+ app-dir definers — promoting one copy into the always-
|
|
272
|
+
# loaded shared bundle would double-declare against the other copy on its page),
|
|
273
|
+
# plus any file that transitively depends on such a file (it can't live in shared
|
|
274
|
+
# if a dependency can't). These fall back to legacy per-consumer inlining.
|
|
275
|
+
def unpromotable_files(file_owner, file_defs, file_uses, symbol_to_file, collided)
|
|
276
|
+
unpromotable = Set.new
|
|
277
|
+
file_owner.each_key do |file_path|
|
|
278
|
+
unpromotable << file_path if file_defs[file_path].any? { |s| collided.include?(s) }
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
loop do
|
|
282
|
+
added = false
|
|
283
|
+
file_owner.each_key do |file_path|
|
|
284
|
+
next if unpromotable.include?(file_path)
|
|
285
|
+
|
|
286
|
+
taints = file_uses.fetch(file_path, Set.new).any? do |sym|
|
|
287
|
+
dep = symbol_to_file[sym]
|
|
288
|
+
dep && unpromotable.include?(dep)
|
|
289
|
+
end
|
|
290
|
+
if taints
|
|
291
|
+
unpromotable << file_path
|
|
292
|
+
added = true
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
break unless added
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
unpromotable
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# A controller file is promoted to the shared bundle when a symbol it canonically
|
|
302
|
+
# defines is used by a bundle other than its owner, then transitively for anything
|
|
303
|
+
# a promoted file depends on. Collided/tainted files (see unpromotable_files) are
|
|
304
|
+
# never promoted. Records why each file was promoted for react_manifest:analyze.
|
|
305
|
+
#
|
|
306
|
+
# symbol_index bundles {file_defs:, symbol_to_file:, symbol_used_by_bundles:} into
|
|
307
|
+
# one param so this stays under Metrics/ParameterLists (rubocop flags 6 positional
|
|
308
|
+
# args) without changing the underlying algorithm.
|
|
309
|
+
def compute_promoted_files(file_owner, file_uses, symbol_index, collided)
|
|
310
|
+
file_defs = symbol_index[:file_defs]
|
|
311
|
+
symbol_to_file = symbol_index[:symbol_to_file]
|
|
312
|
+
symbol_used_by_bundles = symbol_index[:symbol_used_by_bundles]
|
|
313
|
+
unpromotable = unpromotable_files(file_owner, file_defs, file_uses, symbol_to_file, collided)
|
|
314
|
+
promoted = Set.new
|
|
315
|
+
reasons = Hash.new { |h, k| h[k] = Set.new }
|
|
316
|
+
|
|
317
|
+
file_owner.each do |file_path, owner|
|
|
318
|
+
next if unpromotable.include?(file_path)
|
|
319
|
+
|
|
320
|
+
forcing = Set.new
|
|
321
|
+
file_defs[file_path].each do |sym|
|
|
322
|
+
# canonical-definer guard: only the file that canonically defines the
|
|
323
|
+
# symbol is eligible (resolves collisions to one file; excludes isolated
|
|
324
|
+
# dirs, which are never registered in symbol_to_file).
|
|
325
|
+
next unless symbol_to_file[sym] == file_path
|
|
326
|
+
|
|
327
|
+
(symbol_used_by_bundles[sym] - [owner]).each { |b| forcing << b }
|
|
328
|
+
end
|
|
329
|
+
next if forcing.empty?
|
|
330
|
+
|
|
331
|
+
promoted << file_path
|
|
332
|
+
reasons[relative_require_path(file_path)] = forcing
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
worklist = promoted.to_a
|
|
336
|
+
until worklist.empty?
|
|
337
|
+
current = worklist.pop
|
|
338
|
+
file_uses.fetch(current, Set.new).each do |sym|
|
|
339
|
+
dep_file = symbol_to_file[sym]
|
|
340
|
+
next unless dep_file
|
|
341
|
+
next if unpromotable.include?(dep_file)
|
|
342
|
+
next if promoted.include?(dep_file)
|
|
343
|
+
|
|
344
|
+
promoted << dep_file
|
|
345
|
+
reasons[relative_require_path(dep_file)] << "(transitive)"
|
|
346
|
+
worklist << dep_file
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
@promotion_reasons = reasons.transform_values(&:to_a)
|
|
351
|
+
promoted
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Indexes each controller dir's files and the PascalCase symbols they define.
|
|
355
|
+
# Returns [bundle_files, file_owner, file_defs, symbol_to_bundle, symbol_to_file,
|
|
356
|
+
# symbol_definers, bundle_own_symbols].
|
|
357
|
+
def index_controller_symbols(controller_dirs)
|
|
140
358
|
bundle_files = {}
|
|
359
|
+
file_owner = {}
|
|
360
|
+
file_defs = Hash.new { |h, k| h[k] = Set.new }
|
|
141
361
|
symbol_to_bundle = {}
|
|
362
|
+
symbol_to_file = {}
|
|
363
|
+
symbol_definers = Hash.new { |h, k| h[k] = Set.new }
|
|
142
364
|
bundle_own_symbols = Hash.new { |h, k| h[k] = Set.new }
|
|
143
|
-
external_symbol_to_require = {}
|
|
144
|
-
dependencies = Hash.new { |h, k| h[k] = Set.new }
|
|
145
|
-
external_requires = Hash.new { |h, k| h[k] = Set.new }
|
|
146
365
|
|
|
147
366
|
controller_dirs.each do |ctrl|
|
|
148
|
-
# Index controller-defined symbols for cross-app detection
|
|
149
367
|
bundle_name = ctrl[:bundle_name]
|
|
150
368
|
files = js_files_in(ctrl[:path])
|
|
151
369
|
bundle_files[bundle_name] = files
|
|
152
|
-
|
|
153
370
|
isolated = @config.isolated_app_dirs.include?(ctrl[:name])
|
|
154
371
|
|
|
155
372
|
files.each do |file_path|
|
|
373
|
+
file_owner[file_path] = bundle_name
|
|
156
374
|
extract_defined_symbols(file_path).each do |sym|
|
|
157
375
|
next unless sym.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
|
|
158
376
|
|
|
159
|
-
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
377
|
+
file_defs[file_path] << sym
|
|
378
|
+
# Isolated dirs never register into the shared symbol index, so no
|
|
379
|
+
# other bundle can ever be inferred to depend on them.
|
|
380
|
+
unless isolated
|
|
381
|
+
symbol_to_bundle[sym] ||= bundle_name
|
|
382
|
+
symbol_to_file[sym] ||= file_path
|
|
383
|
+
symbol_definers[sym] << file_path
|
|
384
|
+
end
|
|
165
385
|
bundle_own_symbols[bundle_name] << sym
|
|
166
386
|
end
|
|
167
387
|
end
|
|
168
388
|
end
|
|
169
389
|
|
|
170
|
-
|
|
390
|
+
[bundle_files, file_owner, file_defs, symbol_to_bundle, symbol_to_file,
|
|
391
|
+
symbol_definers, bundle_own_symbols]
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Records, per controller file and per bundle, which symbols are used —
|
|
395
|
+
# plus a pseudo-bundle entry (config.shared_bundle) for any controller
|
|
396
|
+
# symbol referenced from a shared-dir file (shared code loads on every
|
|
397
|
+
# page, so that symbol's owning file must be treated as externally used).
|
|
398
|
+
def record_symbol_usages(bundle_files, shared_dirs, file_uses, symbol_used_by_bundles)
|
|
399
|
+
bundle_files.each do |bundle_name, files|
|
|
400
|
+
files.each do |file_path|
|
|
401
|
+
extract_used_component_symbols(file_path).each do |sym|
|
|
402
|
+
file_uses[file_path] << sym
|
|
403
|
+
symbol_used_by_bundles[sym] << bundle_name
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
shared_dirs.each do |dir|
|
|
409
|
+
js_files_in(dir[:path]).each do |file_path|
|
|
410
|
+
extract_used_component_symbols(file_path).each do |sym|
|
|
411
|
+
symbol_used_by_bundles[sym] << @config.shared_bundle
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Indexes symbols from external_roots dirs, then lets explicit
|
|
418
|
+
# external_providers win over scanned roots on symbol conflicts.
|
|
419
|
+
def index_external_symbols(symbol_to_bundle)
|
|
420
|
+
external_symbol_to_require = {}
|
|
421
|
+
|
|
171
422
|
@config.external_roots.each do |root_path|
|
|
172
423
|
abs_root = abs_external_root(root_path)
|
|
173
424
|
external_js_files_in(abs_root).each do |file_path|
|
|
174
425
|
req_path = relative_require_path(file_path)
|
|
175
|
-
|
|
176
426
|
warn_on_external_controller_references(file_path, symbol_to_bundle)
|
|
177
|
-
|
|
178
427
|
extract_defined_symbols(file_path).each do |sym|
|
|
179
428
|
external_symbol_to_require[sym] ||= req_path
|
|
180
429
|
end
|
|
181
430
|
end
|
|
182
431
|
end
|
|
183
432
|
|
|
184
|
-
# Explicit external_providers win over scanned roots on symbol conflicts
|
|
185
433
|
@config.external_providers.each do |sym, req_path|
|
|
186
434
|
external_symbol_to_require[sym] = req_path
|
|
187
435
|
end
|
|
188
436
|
|
|
189
|
-
|
|
437
|
+
external_symbol_to_require
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Cross-app dependency graph + external requires (dependencies kept for
|
|
441
|
+
# reporting). Returns [dependencies, external_requires].
|
|
442
|
+
def build_dependency_graph(bundle_files, bundle_own_symbols, file_uses, symbol_to_bundle,
|
|
443
|
+
external_symbol_to_require)
|
|
444
|
+
dependencies = Hash.new { |h, k| h[k] = Set.new }
|
|
445
|
+
external_requires = Hash.new { |h, k| h[k] = Set.new }
|
|
446
|
+
|
|
190
447
|
bundle_files.each do |bundle_name, files|
|
|
191
448
|
own_symbols = bundle_own_symbols[bundle_name]
|
|
192
|
-
|
|
193
449
|
files.each do |file_path|
|
|
194
|
-
|
|
195
|
-
# A symbol the bundle defines itself (in any of its own files) is
|
|
196
|
-
# always satisfied locally — never attribute it to another bundle
|
|
197
|
-
# or an external provider just because that symbol name happens
|
|
198
|
-
# to collide with something defined elsewhere.
|
|
450
|
+
file_uses[file_path].each do |sym|
|
|
199
451
|
next if own_symbols.include?(sym)
|
|
200
452
|
|
|
201
453
|
dep_bundle = symbol_to_bundle[sym]
|
|
@@ -207,24 +459,82 @@ module ReactManifest
|
|
|
207
459
|
end
|
|
208
460
|
end
|
|
209
461
|
|
|
210
|
-
|
|
462
|
+
[dependencies, external_requires]
|
|
463
|
+
end
|
|
211
464
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
465
|
+
# Under auto_shared, single-definer cross-used files live in ux_shared, so a
|
|
466
|
+
# controller only needs the canonical files for the symbols it uses that are NOT
|
|
467
|
+
# promoted (collided names kept bundle-local, plus their transitive needs).
|
|
468
|
+
# File-level + transitive so we never over-inline a whole dependency bundle
|
|
469
|
+
# (which could double-declare against an always_include bundle's own copy).
|
|
470
|
+
def promoted_aware_dependency_requires(bundle_name, controller_context)
|
|
471
|
+
# Symbols already on every page via an always_include tag. Skipped so a
|
|
472
|
+
# controller never inlines a (possibly colliding) second definer. Disabled
|
|
473
|
+
# when building an always_include bundle's own manifest (it needs its deps).
|
|
474
|
+
always_provided = always_provided_for(bundle_name, controller_context)
|
|
475
|
+
promoted_aware_dependency_files(bundle_name, controller_context, always_provided)
|
|
476
|
+
.map { |f| relative_require_path(f) }
|
|
477
|
+
.uniq
|
|
478
|
+
.sort
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Absolute paths of the non-promoted files a bundle must inline: for each
|
|
482
|
+
# symbol it uses (transitively) that is neither its own, promoted to shared,
|
|
483
|
+
# nor +always_provided+ on the page, the canonical (first-writer) definer file.
|
|
484
|
+
def promoted_aware_dependency_files(bundle_name, controller_context, always_provided)
|
|
485
|
+
own = controller_context[:bundle_own_symbols].fetch(bundle_name, Set.new)
|
|
486
|
+
promoted = controller_context[:promoted_files]
|
|
487
|
+
file_uses = controller_context[:file_uses]
|
|
488
|
+
sym2file = controller_context[:symbol_to_file]
|
|
489
|
+
own_files = controller_context[:bundle_files].fetch(bundle_name, [])
|
|
490
|
+
|
|
491
|
+
needed = Set.new
|
|
492
|
+
seen = Set.new
|
|
493
|
+
worklist = own_files.flat_map { |f| file_uses.fetch(f, Set.new).to_a }
|
|
494
|
+
|
|
495
|
+
until worklist.empty?
|
|
496
|
+
sym = worklist.pop
|
|
497
|
+
next if seen.include?(sym)
|
|
498
|
+
|
|
499
|
+
seen << sym
|
|
500
|
+
next if own.include?(sym)
|
|
501
|
+
next if always_provided.include?(sym)
|
|
502
|
+
|
|
503
|
+
dep_file = sym2file[sym]
|
|
504
|
+
next unless dep_file
|
|
505
|
+
next if promoted.include?(dep_file) # already served by ux_shared
|
|
506
|
+
next if own_files.include?(dep_file) # own file, added via own_requires
|
|
507
|
+
next if needed.include?(dep_file)
|
|
508
|
+
|
|
509
|
+
needed << dep_file
|
|
510
|
+
file_uses.fetch(dep_file, Set.new).each { |s| worklist << s unless seen.include?(s) }
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
needed
|
|
218
514
|
end
|
|
219
515
|
|
|
220
516
|
def controller_dependency_requires(bundle_name, controller_context)
|
|
221
517
|
deps = transitive_dependencies(bundle_name, controller_context[:dependencies])
|
|
518
|
+
# Drop always_include bundles (and their transitive deps): they load on
|
|
519
|
+
# every page via their own <script> tag, so inlining them here would
|
|
520
|
+
# double-declare. Kept when the current bundle IS an always_include bundle.
|
|
521
|
+
always = controller_context[:always_loaded_bundles]
|
|
522
|
+
deps -= always.to_a unless always.include?(bundle_name)
|
|
222
523
|
deps.flat_map { |dep_bundle| controller_context[:bundle_files].fetch(dep_bundle, []) }
|
|
223
524
|
.map { |abs_path| relative_require_path(abs_path) }
|
|
224
525
|
.uniq
|
|
225
526
|
.sort
|
|
226
527
|
end
|
|
227
528
|
|
|
529
|
+
# Symbols delivered on every page by an always_include tag, for +bundle_name+.
|
|
530
|
+
# Empty when building an always_include bundle's own manifest so it still
|
|
531
|
+
# inlines the dependencies it needs.
|
|
532
|
+
def always_provided_for(bundle_name, controller_context)
|
|
533
|
+
return Set.new if controller_context[:always_loaded_bundles].include?(bundle_name)
|
|
534
|
+
|
|
535
|
+
controller_context[:always_provided_symbols]
|
|
536
|
+
end
|
|
537
|
+
|
|
228
538
|
def transitive_dependencies(bundle_name, dependency_map)
|
|
229
539
|
ordered = []
|
|
230
540
|
visiting = Set.new
|
|
@@ -245,30 +555,16 @@ module ReactManifest
|
|
|
245
555
|
ordered
|
|
246
556
|
end
|
|
247
557
|
|
|
248
|
-
|
|
558
|
+
# The set of bundles guaranteed loaded on every page by always_include: each
|
|
559
|
+
# configured always_include bundle plus its transitive dependencies. These
|
|
560
|
+
# arrive via their own <script> tag (resolve_bundles / react_component), so
|
|
561
|
+
# controller manifests must never inline their files or symbols.
|
|
562
|
+
def compute_always_loaded_bundles(dependencies)
|
|
249
563
|
bundles = @config.always_include.map(&:to_s).reject(&:empty?).uniq
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
bundle_files.each_key do |bundle_name|
|
|
255
|
-
requires = Set.new
|
|
256
|
-
|
|
257
|
-
bundles.each do |always_bundle|
|
|
258
|
-
next if always_bundle == bundle_name
|
|
259
|
-
|
|
260
|
-
transitive = [always_bundle] + transitive_dependencies(always_bundle, dependencies)
|
|
261
|
-
transitive.each do |dep_bundle|
|
|
262
|
-
bundle_files.fetch(dep_bundle, []).each do |abs_path|
|
|
263
|
-
requires << relative_require_path(abs_path)
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
requires_by_bundle[bundle_name] = requires.to_a.sort
|
|
564
|
+
bundles.each_with_object(Set.new) do |always_bundle, acc|
|
|
565
|
+
acc << always_bundle
|
|
566
|
+
transitive_dependencies(always_bundle, dependencies).each { |dep| acc << dep }
|
|
269
567
|
end
|
|
270
|
-
|
|
271
|
-
requires_by_bundle
|
|
272
568
|
end
|
|
273
569
|
|
|
274
570
|
# --------------------------------------------------------------- write
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module ReactManifest
|
|
4
|
+
# Ensures the generated manifest dir is gitignored and that a committed .keep
|
|
5
|
+
# keeps the directory present on a fresh clone. Idempotent and best-effort:
|
|
6
|
+
# a filesystem error logs a warning and never raises.
|
|
7
|
+
class GitignorePatcher
|
|
8
|
+
include ReactManifest::Logging
|
|
9
|
+
|
|
10
|
+
Result = Struct.new(:appended, :keep_created, keyword_init: true)
|
|
11
|
+
|
|
12
|
+
def initialize(config = ReactManifest.configuration)
|
|
13
|
+
@config = config
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def patch!
|
|
17
|
+
Result.new(appended: ensure_gitignore_entry, keep_created: ensure_keep)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# patch! plus a one-time hint to untrack previously-committed manifests when the
|
|
21
|
+
# .gitignore entry was just added (the gem never runs git itself). Returns true
|
|
22
|
+
# iff the .gitignore entry was appended.
|
|
23
|
+
def reconcile!
|
|
24
|
+
result = patch!
|
|
25
|
+
if result.appended
|
|
26
|
+
base = File.dirname(pattern)
|
|
27
|
+
log_info "If #{base}/*.js were previously committed, untrack them once: " \
|
|
28
|
+
"git rm --cached #{base}/*.js"
|
|
29
|
+
end
|
|
30
|
+
result.appended
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Path (relative to Rails.root) that should be ignored, e.g.
|
|
34
|
+
# "app/assets/javascripts/ux_manifests/*.js".
|
|
35
|
+
def pattern
|
|
36
|
+
subdir = @config.normalized_manifest_subdir
|
|
37
|
+
base = subdir.empty? ? @config.output_dir : File.join(@config.output_dir, subdir)
|
|
38
|
+
File.join(base, "*.js")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def gitignore_path
|
|
44
|
+
Rails.root.join(".gitignore").to_s
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ensure_gitignore_entry
|
|
48
|
+
path = gitignore_path
|
|
49
|
+
line = pattern
|
|
50
|
+
content = File.exist?(path) ? File.read(path, encoding: "utf-8") : ""
|
|
51
|
+
return false if content.split("\n").map(&:strip).include?(line)
|
|
52
|
+
|
|
53
|
+
prefix = content.empty? || content.end_with?("\n") ? "" : "\n"
|
|
54
|
+
File.open(path, "a") { |f| f.write("#{prefix}#{line}\n") }
|
|
55
|
+
log_info "Added #{line} to .gitignore"
|
|
56
|
+
true
|
|
57
|
+
rescue Errno::EACCES, Errno::ENOENT => e
|
|
58
|
+
log_warn "Could not update .gitignore: #{e.message}"
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def ensure_keep
|
|
63
|
+
dir = @config.abs_manifest_dir
|
|
64
|
+
keep = File.join(dir, ".keep")
|
|
65
|
+
return false if File.exist?(keep)
|
|
66
|
+
|
|
67
|
+
FileUtils.mkdir_p(dir)
|
|
68
|
+
FileUtils.touch(keep)
|
|
69
|
+
true
|
|
70
|
+
rescue Errno::EACCES => e
|
|
71
|
+
log_warn "Could not create #{dir}/.keep: #{e.message}"
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -26,6 +26,12 @@ module ReactManifest
|
|
|
26
26
|
Rails.logger&.warn(error)
|
|
27
27
|
$stdout.puts(error) if config.stdout_logging?
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
ReactManifest.reconcile_gitignore!(config)
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
Rails.logger&.warn("[ReactManifest] gitignore reconcile failed: #{e.message}")
|
|
34
|
+
end
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
# Expose config as Rails.application.config.react_manifest
|
data/lib/react_manifest.rb
CHANGED
|
@@ -14,6 +14,7 @@ require "react_manifest/application_analyzer"
|
|
|
14
14
|
require "react_manifest/application_migrator"
|
|
15
15
|
require "react_manifest/layout_patcher"
|
|
16
16
|
require "react_manifest/sprockets_manifest_patcher"
|
|
17
|
+
require "react_manifest/gitignore_patcher"
|
|
17
18
|
require "react_manifest/watcher"
|
|
18
19
|
require "react_manifest/reporter"
|
|
19
20
|
require "react_manifest/view_helpers"
|
|
@@ -55,10 +56,7 @@ module ReactManifest
|
|
|
55
56
|
bundles << shared if shared
|
|
56
57
|
|
|
57
58
|
# 2. always_include bundles (e.g. ux_main)
|
|
58
|
-
config
|
|
59
|
-
resolved = resolve_bundle_reference(config, b)
|
|
60
|
-
bundles << resolved if resolved && !bundles.include?(resolved)
|
|
61
|
-
end
|
|
59
|
+
append_always_include(config, bundles)
|
|
62
60
|
|
|
63
61
|
# 3. Controller-specific bundle
|
|
64
62
|
# Try fully-namespaced first: admin/users → ux_admin_users
|
|
@@ -74,6 +72,18 @@ module ReactManifest
|
|
|
74
72
|
bundles
|
|
75
73
|
end
|
|
76
74
|
|
|
75
|
+
# Ensure the manifest dir is gitignored (and .keep present). Honors
|
|
76
|
+
# config.manage_gitignore. Returns true if it appended the .gitignore entry.
|
|
77
|
+
# Best-effort: a filesystem error is logged and swallowed (never breaks boot).
|
|
78
|
+
def reconcile_gitignore!(config = configuration)
|
|
79
|
+
return false unless config.manage_gitignore?
|
|
80
|
+
|
|
81
|
+
GitignorePatcher.new(config).reconcile!
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
Rails.logger&.warn("[ReactManifest] gitignore reconcile skipped: #{e.message}")
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
|
|
77
87
|
# Resolve a controller bundle from a React component symbol.
|
|
78
88
|
#
|
|
79
89
|
# This is primarily used to support react-rails `react_component` calls,
|
|
@@ -102,6 +112,12 @@ module ReactManifest
|
|
|
102
112
|
shared = resolve_bundle_reference(config, config.shared_bundle)
|
|
103
113
|
bundles << shared if shared
|
|
104
114
|
|
|
115
|
+
# always_include bundles are delivered on every page. react_component does
|
|
116
|
+
# NOT go through resolve_bundles, so it must emit them here too — this is
|
|
117
|
+
# what makes always_include symbols available without inlining them into
|
|
118
|
+
# controller manifests (which would double-declare, see Generator).
|
|
119
|
+
append_always_include(config, bundles)
|
|
120
|
+
|
|
105
121
|
root = resolve_bundle_reference(config, root_bundle)
|
|
106
122
|
bundles << root if root && !bundles.include?(root)
|
|
107
123
|
|
|
@@ -142,6 +158,15 @@ module ReactManifest
|
|
|
142
158
|
|
|
143
159
|
private
|
|
144
160
|
|
|
161
|
+
# Append each configured always_include bundle (deduped) to +bundles+.
|
|
162
|
+
def append_always_include(config, bundles)
|
|
163
|
+
config.always_include.each do |b|
|
|
164
|
+
resolved = resolve_bundle_reference(config, b)
|
|
165
|
+
bundles << resolved if resolved && !bundles.include?(resolved)
|
|
166
|
+
end
|
|
167
|
+
bundles
|
|
168
|
+
end
|
|
169
|
+
|
|
145
170
|
def component_bundle_map(config)
|
|
146
171
|
component_maps(config)[:symbol_to_bundle]
|
|
147
172
|
end
|
data/tasks/react_manifest.rake
CHANGED
|
@@ -80,6 +80,22 @@ namespace :react_manifest do
|
|
|
80
80
|
puts " ✓ #{written} written, #{unchanged} unchanged"
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# 6. Ensure generated manifests are gitignored
|
|
84
|
+
puts "\n6) Gitignoring generated manifests..."
|
|
85
|
+
if config.manage_gitignore?
|
|
86
|
+
patcher = ReactManifest::GitignorePatcher.new(config)
|
|
87
|
+
result = patcher.patch!
|
|
88
|
+
if result.appended
|
|
89
|
+
puts " ✓ added #{patcher.pattern} to .gitignore"
|
|
90
|
+
puts " → one-time cleanup for previously-committed manifests:"
|
|
91
|
+
puts " git rm --cached #{File.dirname(patcher.pattern)}/*.js"
|
|
92
|
+
else
|
|
93
|
+
puts " ✓ #{patcher.pattern} already ignored"
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
puts " (skipped — config.manage_gitignore is false)"
|
|
97
|
+
end
|
|
98
|
+
|
|
83
99
|
# Done
|
|
84
100
|
puts "\n=== Setup #{'(dry-run) ' if config.dry_run?}complete ==="
|
|
85
101
|
puts "Start your Rails server — react_bundle_tag will serve the right JS per controller." unless config.dry_run?
|
|
@@ -130,6 +146,14 @@ namespace :react_manifest do
|
|
|
130
146
|
scan_result = scanner.scan(classification)
|
|
131
147
|
dep_map = ReactManifest::DependencyMap.new(scan_result)
|
|
132
148
|
dep_map.print_report
|
|
149
|
+
|
|
150
|
+
reasons = ReactManifest::Generator.new(config).promotion_reasons
|
|
151
|
+
unless reasons.empty?
|
|
152
|
+
puts "\nPromotions (app/ components emitted into #{config.shared_bundle}):"
|
|
153
|
+
reasons.sort.each do |req, bundles|
|
|
154
|
+
puts " #{req} <- used by: #{bundles.sort.join(', ')}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
133
157
|
end
|
|
134
158
|
|
|
135
159
|
desc "Analyze application*.js files — show what migrate_application would change"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: react-manifest-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
@@ -155,6 +155,7 @@ files:
|
|
|
155
155
|
- lib/react_manifest/configuration.rb
|
|
156
156
|
- lib/react_manifest/dependency_map.rb
|
|
157
157
|
- lib/react_manifest/generator.rb
|
|
158
|
+
- lib/react_manifest/gitignore_patcher.rb
|
|
158
159
|
- lib/react_manifest/layout_patcher.rb
|
|
159
160
|
- lib/react_manifest/logging.rb
|
|
160
161
|
- lib/react_manifest/path_utils.rb
|