react-manifest-rails 0.2.34 → 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 +87 -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/configuration.rb +19 -0
- data/lib/react_manifest/generator.rb +477 -83
- data/lib/react_manifest/gitignore_patcher.rb +81 -0
- data/lib/react_manifest/railtie.rb +6 -0
- 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 +31 -75
- data/tasks/react_manifest.rake +37 -0
- metadata +17 -15
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
# The component-index sidecar written next to the manifests (see ComponentIndex).
|
|
34
|
+
def json_pattern
|
|
35
|
+
File.join(File.dirname(pattern), "*.json")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Path (relative to Rails.root) that should be ignored, e.g.
|
|
39
|
+
# "app/assets/javascripts/ux_manifests/*.js".
|
|
40
|
+
def pattern
|
|
41
|
+
subdir = @config.normalized_manifest_subdir
|
|
42
|
+
base = subdir.empty? ? @config.output_dir : File.join(@config.output_dir, subdir)
|
|
43
|
+
File.join(base, "*.js")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def gitignore_path
|
|
49
|
+
Rails.root.join(".gitignore").to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ensure_gitignore_entry
|
|
53
|
+
path = gitignore_path
|
|
54
|
+
content = File.exist?(path) ? File.read(path, encoding: "utf-8") : ""
|
|
55
|
+
lines = content.split("\n").map(&:strip)
|
|
56
|
+
missing = [pattern, json_pattern] - lines
|
|
57
|
+
return false if missing.empty?
|
|
58
|
+
|
|
59
|
+
prefix = content.empty? || content.end_with?("\n") ? "" : "\n"
|
|
60
|
+
File.open(path, "a") { |f| f.write("#{prefix}#{missing.join("\n")}\n") }
|
|
61
|
+
log_info "Added #{missing.join(', ')} to .gitignore"
|
|
62
|
+
true
|
|
63
|
+
rescue Errno::EACCES, Errno::ENOENT => e
|
|
64
|
+
log_warn "Could not update .gitignore: #{e.message}"
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def ensure_keep
|
|
69
|
+
dir = @config.abs_manifest_dir
|
|
70
|
+
keep = File.join(dir, ".keep")
|
|
71
|
+
return false if File.exist?(keep)
|
|
72
|
+
|
|
73
|
+
FileUtils.mkdir_p(dir)
|
|
74
|
+
FileUtils.touch(keep)
|
|
75
|
+
true
|
|
76
|
+
rescue Errno::EACCES => e
|
|
77
|
+
log_warn "Could not create #{dir}/.keep: #{e.message}"
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
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
|
|
@@ -44,6 +44,20 @@ module ReactManifest
|
|
|
44
44
|
regex_extract_definitions(content)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# Top-level const/let/class names of any casing (destructured ones included).
|
|
48
|
+
# These are the only declarations that throw when two classic scripts share a
|
|
49
|
+
# global lexical scope; var and function redeclaration is legal.
|
|
50
|
+
def extract_lexical_bindings(content, file_path: nil)
|
|
51
|
+
return [] unless content
|
|
52
|
+
|
|
53
|
+
if ast_engine_available?
|
|
54
|
+
result = AstExtractor.extract_lexical_bindings(content, file_path: file_path)
|
|
55
|
+
return result if result
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
regex_extract_lexical_bindings(content)
|
|
59
|
+
end
|
|
60
|
+
|
|
47
61
|
def extract_usages(content, file_path: nil)
|
|
48
62
|
return [] unless content
|
|
49
63
|
|
|
@@ -75,6 +89,19 @@ module ReactManifest
|
|
|
75
89
|
symbols.uniq
|
|
76
90
|
end
|
|
77
91
|
|
|
92
|
+
# ponytail: column-0 declarations only, flat destructuring; the AST path is exact.
|
|
93
|
+
def regex_extract_lexical_bindings(content)
|
|
94
|
+
names = content.scan(/^(?:export\s+)?class\s+([A-Za-z_$][\w$]*)/).flatten
|
|
95
|
+
content.scan(/^(?:export\s+)?(?:const|let)\s+([A-Za-z_$][\w$]*)/) { |m| names << m[0] }
|
|
96
|
+
content.scan(/^(?:export\s+)?(?:const|let)\s*[{\[]([^}\]]*)[}\]]\s*=[^=]/) do |m|
|
|
97
|
+
m[0].split(",").each do |part|
|
|
98
|
+
name = part.split("=").first.split(":").last.to_s.sub("...", "").strip
|
|
99
|
+
names << name if name.match?(/\A[A-Za-z_$][\w$]*\z/)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
names.uniq
|
|
103
|
+
end
|
|
104
|
+
|
|
78
105
|
def regex_extract_usages(content)
|
|
79
106
|
local_syms = Set.new
|
|
80
107
|
DEFINITION_PATTERNS.each { |p| content.scan(p) { |m| local_syms << m[0] } }
|