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.
@@ -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] } }