react-manifest-rails 0.3.0 → 0.3.1
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 +40 -0
- data/README.md +7 -2
- data/lib/react_manifest/ast_extractor.rb +11 -0
- data/lib/react_manifest/component_index.rb +136 -0
- data/lib/react_manifest/generator.rb +147 -49
- data/lib/react_manifest/gitignore_patcher.rb +10 -4
- data/lib/react_manifest/symbol_extractor.rb +27 -0
- data/lib/react_manifest/vendor/ast_extractor.js +11 -11
- data/lib/react_manifest/vendor/build/entry.js +32 -0
- data/lib/react_manifest/version.rb +1 -1
- data/lib/react_manifest/view_helpers.rb +42 -5
- data/lib/react_manifest.rb +2 -71
- data/tasks/react_manifest.rake +13 -0
- metadata +16 -15
|
@@ -52,6 +52,31 @@ function definitionsFromAst(ast) {
|
|
|
52
52
|
return [...new Set(defs)];
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Top-level const/let/class binding names of any casing, including destructured
|
|
56
|
+
// ones. Only these throw "Identifier already declared" when two classic scripts
|
|
57
|
+
// share a global lexical scope; var/function redeclaration is legal, so skipped.
|
|
58
|
+
function lexicalBindingsFromAst(ast) {
|
|
59
|
+
const names = [];
|
|
60
|
+
const collect = (p) => {
|
|
61
|
+
if (!p) return;
|
|
62
|
+
switch (p.type) {
|
|
63
|
+
case "Identifier": names.push(p.name); break;
|
|
64
|
+
case "ObjectPattern": p.properties.forEach((prop) => collect(prop.type === "RestElement" ? prop.argument : prop.value)); break;
|
|
65
|
+
case "ArrayPattern": p.elements.forEach(collect); break;
|
|
66
|
+
case "AssignmentPattern": collect(p.left); break;
|
|
67
|
+
case "RestElement": collect(p.argument); break;
|
|
68
|
+
default: break;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
for (const node of ast.body) {
|
|
72
|
+
const decl = node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration" ? node.declaration : node;
|
|
73
|
+
if (!decl) continue;
|
|
74
|
+
if (decl.type === "VariableDeclaration" && decl.kind !== "var") decl.declarations.forEach((d) => collect(d.id));
|
|
75
|
+
else if (decl.type === "ClassDeclaration") collect(decl.id);
|
|
76
|
+
}
|
|
77
|
+
return [...new Set(names)];
|
|
78
|
+
}
|
|
79
|
+
|
|
55
80
|
// Real reference positions only: JSX element names, call/new callees, and
|
|
56
81
|
// generic identifier references (prop values, array elements, assignments,
|
|
57
82
|
// arguments) — explicitly excluding JSXText and StringLiteral content. This
|
|
@@ -124,6 +149,13 @@ globalThis.__astExtractor = {
|
|
|
124
149
|
return { success: false, error: errorInfo(e) };
|
|
125
150
|
}
|
|
126
151
|
},
|
|
152
|
+
extractLexicalBindings(content) {
|
|
153
|
+
try {
|
|
154
|
+
return { success: true, bindings: lexicalBindingsFromAst(parse(content)) };
|
|
155
|
+
} catch (e) {
|
|
156
|
+
return { success: false, error: errorInfo(e) };
|
|
157
|
+
}
|
|
158
|
+
},
|
|
127
159
|
extractUsages(content) {
|
|
128
160
|
try {
|
|
129
161
|
const ast = parse(content);
|
|
@@ -10,25 +10,36 @@ module ReactManifest
|
|
|
10
10
|
# 3. For namespaced controllers (admin/users): checks ux_admin_users, then ux_admin
|
|
11
11
|
# 4. Returns "" for pure ERB/HAML pages with no matching bundle, or when
|
|
12
12
|
# called outside a controller context (mailers, engines, etc.)
|
|
13
|
+
#
|
|
14
|
+
# Rails renders the action template before the layout, so react_component
|
|
15
|
+
# tags land in the body first. react_bundle_tag hoists them into its own
|
|
16
|
+
# output (the layout's <head>) so Turbolinks never re-evaluates a body script.
|
|
13
17
|
module ViewHelpers
|
|
18
|
+
INLINE_BUNDLE_TAG = %r{<script\b[^>]*\sdata-react-manifest="([^"]*)"[^>]*></script>}
|
|
19
|
+
|
|
14
20
|
def react_bundle_tag(**html_options)
|
|
15
21
|
# controller_path is not available in all view contexts (mailers, engines,
|
|
16
22
|
# views rendered outside a request). Fall back gracefully rather than raising.
|
|
17
23
|
ctrl = respond_to?(:controller_path, true) ? controller_path : nil
|
|
18
24
|
return "".html_safe if ctrl.nil?
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
emitted = emitted_bundles
|
|
27
|
+
hoisted = hoist_inline_bundle_tags
|
|
28
|
+
emitted.reject! { |e| hoisted.any? { |h| canonical_bundle_name(h) == canonical_bundle_name(e) } }
|
|
29
|
+
|
|
30
|
+
bundles = (ReactManifest.resolve_bundles(ctrl) + hoisted).uniq { |b| canonical_bundle_name(b) }
|
|
21
31
|
return "".html_safe if bundles.empty?
|
|
22
32
|
|
|
23
33
|
# Record emitted bundles so react_component doesn't re-emit them.
|
|
24
|
-
emitted = emitted_bundles
|
|
25
34
|
fresh_bundles = bundles.reject { |b| emitted_bundle?(emitted, b) }
|
|
26
35
|
return "".html_safe if fresh_bundles.empty?
|
|
27
36
|
|
|
28
37
|
fresh_bundles.each { |b| emitted << b }
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
tags = fresh_bundles.map do |bundle|
|
|
40
|
+
javascript_include_tag("#{bundle}.js", extname: false, **bundle_tag_options(bundle, html_options))
|
|
41
|
+
end
|
|
42
|
+
safe_join(tags, "\n")
|
|
32
43
|
end
|
|
33
44
|
|
|
34
45
|
# react-rails integration:
|
|
@@ -48,7 +59,7 @@ module ReactManifest
|
|
|
48
59
|
next if emitted_bundle?(emitted, bundle)
|
|
49
60
|
|
|
50
61
|
emitted << bundle
|
|
51
|
-
javascript_include_tag("#{bundle}.js", extname: false)
|
|
62
|
+
javascript_include_tag("#{bundle}.js", extname: false, data: { react_manifest: bundle })
|
|
52
63
|
end
|
|
53
64
|
|
|
54
65
|
return html if new_tags.empty?
|
|
@@ -58,6 +69,32 @@ module ReactManifest
|
|
|
58
69
|
|
|
59
70
|
private
|
|
60
71
|
|
|
72
|
+
# Turbolinks 5 full-reloads when the tracked head elements differ between pages.
|
|
73
|
+
# ux_shared is identical on every page, so tracking it forces one reload per deploy;
|
|
74
|
+
# tracking controller bundles would reload on every cross-controller visit.
|
|
75
|
+
def bundle_tag_options(bundle, html_options)
|
|
76
|
+
shared = canonical_bundle_name(ReactManifest.configuration.shared_bundle)
|
|
77
|
+
return html_options unless canonical_bundle_name(bundle) == shared
|
|
78
|
+
|
|
79
|
+
html_options.merge(data: (html_options[:data] || {}).merge(turbolinks_track: "reload"))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Strips the inline tags react_component already wrote into the rendered
|
|
83
|
+
# template (and content_for) buffers; returns their bundle names in order.
|
|
84
|
+
def hoist_inline_bundle_tags
|
|
85
|
+
return [] unless respond_to?(:view_flow, true) && view_flow
|
|
86
|
+
|
|
87
|
+
names = []
|
|
88
|
+
view_flow.content.each_key do |key|
|
|
89
|
+
html = view_flow.get(key).to_s
|
|
90
|
+
next unless html.include?("data-react-manifest")
|
|
91
|
+
|
|
92
|
+
names.concat(html.scan(INLINE_BUNDLE_TAG).flatten)
|
|
93
|
+
view_flow.set(key, html.gsub(INLINE_BUNDLE_TAG, "").html_safe)
|
|
94
|
+
end
|
|
95
|
+
names
|
|
96
|
+
end
|
|
97
|
+
|
|
61
98
|
def emitted_bundle?(emitted, bundle)
|
|
62
99
|
canonical = canonical_bundle_name(bundle)
|
|
63
100
|
emitted.any? { |existing| canonical_bundle_name(existing) == canonical }
|
data/lib/react_manifest.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "react_manifest/configuration"
|
|
|
7
7
|
require "react_manifest/path_utils"
|
|
8
8
|
require "react_manifest/tree_classifier"
|
|
9
9
|
require "react_manifest/symbol_extractor"
|
|
10
|
+
require "react_manifest/component_index"
|
|
10
11
|
require "react_manifest/scanner"
|
|
11
12
|
require "react_manifest/dependency_map"
|
|
12
13
|
require "react_manifest/generator"
|
|
@@ -19,7 +20,6 @@ require "react_manifest/watcher"
|
|
|
19
20
|
require "react_manifest/reporter"
|
|
20
21
|
require "react_manifest/view_helpers"
|
|
21
22
|
|
|
22
|
-
# rubocop:disable Metrics/ModuleLength
|
|
23
23
|
module ReactManifest
|
|
24
24
|
class << self
|
|
25
25
|
def configuration
|
|
@@ -178,79 +178,11 @@ module ReactManifest
|
|
|
178
178
|
return cached_maps if cached_key == key
|
|
179
179
|
end
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
symbol_to_bundle = {}
|
|
183
|
-
bundle_own_symbols = Hash.new { |h, k| h[k] = Set.new }
|
|
184
|
-
bundle_files = Hash.new { |h, k| h[k] = [] }
|
|
185
|
-
bundle_dependencies = Hash.new { |h, k| h[k] = Set.new }
|
|
186
|
-
|
|
187
|
-
controller_dirs.each do |ctrl|
|
|
188
|
-
bundle_name = ctrl[:bundle_name]
|
|
189
|
-
files = js_files_in_controller(ctrl[:path], config)
|
|
190
|
-
bundle_files[bundle_name].concat(files)
|
|
191
|
-
isolated = config.isolated_app_dirs.include?(ctrl[:name])
|
|
192
|
-
|
|
193
|
-
files.each do |file_path|
|
|
194
|
-
extract_defined_symbols(file_path).each do |symbol|
|
|
195
|
-
next unless symbol.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
|
|
196
|
-
|
|
197
|
-
# Keep first writer to ensure deterministic behavior if a symbol is duplicated.
|
|
198
|
-
# Isolated dirs never register into the shared symbol index (see
|
|
199
|
-
# Generator#build_controller_context for the full rationale).
|
|
200
|
-
symbol_to_bundle[symbol] ||= bundle_name unless isolated
|
|
201
|
-
bundle_own_symbols[bundle_name] << symbol
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
bundle_files.each do |bundle_name, files|
|
|
207
|
-
own_symbols = bundle_own_symbols[bundle_name]
|
|
208
|
-
|
|
209
|
-
files.each do |file_path|
|
|
210
|
-
extract_used_component_symbols(file_path).each do |symbol|
|
|
211
|
-
# A symbol the bundle defines itself is always satisfied locally —
|
|
212
|
-
# never attribute it to another bundle just because that symbol
|
|
213
|
-
# name happens to collide with something defined elsewhere.
|
|
214
|
-
next if own_symbols.include?(symbol)
|
|
215
|
-
|
|
216
|
-
dep_bundle = symbol_to_bundle[symbol]
|
|
217
|
-
next unless dep_bundle && dep_bundle != bundle_name
|
|
218
|
-
|
|
219
|
-
bundle_dependencies[bundle_name] << dep_bundle
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
maps = {
|
|
225
|
-
symbol_to_bundle: symbol_to_bundle,
|
|
226
|
-
bundle_dependencies: bundle_dependencies
|
|
227
|
-
}
|
|
181
|
+
maps = ComponentIndex.maps(config)
|
|
228
182
|
@component_maps_cache = [key, maps]
|
|
229
183
|
maps
|
|
230
184
|
end
|
|
231
185
|
|
|
232
|
-
def js_files_in_controller(dir, config)
|
|
233
|
-
return [] unless Dir.exist?(dir)
|
|
234
|
-
|
|
235
|
-
Dir.glob(File.join(dir, "**", config.extensions_glob))
|
|
236
|
-
.reject { |f| File.directory?(f) }
|
|
237
|
-
.sort
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def extract_defined_symbols(file_path)
|
|
241
|
-
content = File.read(file_path, encoding: "utf-8")
|
|
242
|
-
SymbolExtractor.extract_definitions(content, file_path: file_path)
|
|
243
|
-
rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
|
|
244
|
-
[]
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
def extract_used_component_symbols(file_path)
|
|
248
|
-
content = File.read(file_path, encoding: "utf-8")
|
|
249
|
-
SymbolExtractor.extract_usages(content, file_path: file_path)
|
|
250
|
-
rescue Errno::ENOENT, Errno::EACCES, Encoding::InvalidByteSequenceError
|
|
251
|
-
[]
|
|
252
|
-
end
|
|
253
|
-
|
|
254
186
|
def resolve_bundle_reference(config, bundle_name)
|
|
255
187
|
subdir = config.normalized_manifest_subdir
|
|
256
188
|
manifest_path = File.join(config.abs_manifest_dir, "#{bundle_name}.js")
|
|
@@ -286,4 +218,3 @@ module ReactManifest
|
|
|
286
218
|
end
|
|
287
219
|
end
|
|
288
220
|
end
|
|
289
|
-
# rubocop:enable Metrics/ModuleLength
|
data/tasks/react_manifest.rake
CHANGED
|
@@ -136,6 +136,19 @@ namespace :react_manifest do
|
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
+
desc "Fail (exit 1, warnings on stderr) if any bundle declares the same top-level binding twice; writes nothing"
|
|
140
|
+
task check: :environment do
|
|
141
|
+
config = ReactManifest.configuration
|
|
142
|
+
next unless Dir.exist?(config.abs_ux_root)
|
|
143
|
+
|
|
144
|
+
clashes = ReactManifest::Generator.new(config).check
|
|
145
|
+
next if clashes.empty?
|
|
146
|
+
|
|
147
|
+
clashes.each { |message| warn "[ReactManifest] #{message}" }
|
|
148
|
+
warn "[ReactManifest] #{clashes.size} declaration clash(es) found."
|
|
149
|
+
exit 1
|
|
150
|
+
end
|
|
151
|
+
|
|
139
152
|
desc "Print the JSX dependency map and warnings without writing any files"
|
|
140
153
|
task analyze: :environment do
|
|
141
154
|
config = ReactManifest.configuration
|
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.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Oliver Noonan
|
|
@@ -29,6 +29,20 @@ dependencies:
|
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '9'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: mini_racer
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '0.2'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0.2'
|
|
32
46
|
- !ruby/object:Gem::Dependency
|
|
33
47
|
name: minitest
|
|
34
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -119,20 +133,6 @@ dependencies:
|
|
|
119
133
|
- - "~>"
|
|
120
134
|
- !ruby/object:Gem::Version
|
|
121
135
|
version: '3.0'
|
|
122
|
-
- !ruby/object:Gem::Dependency
|
|
123
|
-
name: mini_racer
|
|
124
|
-
requirement: !ruby/object:Gem::Requirement
|
|
125
|
-
requirements:
|
|
126
|
-
- - ">="
|
|
127
|
-
- !ruby/object:Gem::Version
|
|
128
|
-
version: '0'
|
|
129
|
-
type: :development
|
|
130
|
-
prerelease: false
|
|
131
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
132
|
-
requirements:
|
|
133
|
-
- - ">="
|
|
134
|
-
- !ruby/object:Gem::Version
|
|
135
|
-
version: '0'
|
|
136
136
|
description: |
|
|
137
137
|
react-manifest-rails automatically generates per-controller Sprockets manifest
|
|
138
138
|
files for Rails applications using react-rails + Sprockets. It eliminates the
|
|
@@ -152,6 +152,7 @@ files:
|
|
|
152
152
|
- lib/react_manifest/application_analyzer.rb
|
|
153
153
|
- lib/react_manifest/application_migrator.rb
|
|
154
154
|
- lib/react_manifest/ast_extractor.rb
|
|
155
|
+
- lib/react_manifest/component_index.rb
|
|
155
156
|
- lib/react_manifest/configuration.rb
|
|
156
157
|
- lib/react_manifest/dependency_map.rb
|
|
157
158
|
- lib/react_manifest/generator.rb
|