Module: SorbetErb
- Defined in:
- lib/sorbet_erb.rb,
lib/sorbet_erb/version.rb,
lib/sorbet_erb/code_extractor.rb
Defined Under Namespace
Classes: CodeExtractor, CodeProcessor
Constant Summary collapse
- CONFIG_FILE_NAME =
'.sorbet_erb.yml'- DEFAULT_CONFIG =
{ 'input_dirs' => ['app'], 'exclude_paths' => [], 'output_dir' => 'sorbet/erb', 'extra_includes' => [], 'extra_body' => '', 'skip_missing_locals' => true }.freeze
- USAGE =
"Usage: sorbet_erb input_dir output_dir\n input_dir - where to scan for ERB files\n output_dir - where to write files with Ruby extracted from ERB\n"- ERB_TEMPLATE =
"# typed: true\nclass <%= class_name %><%= extend_app_controller ? \" < ApplicationController\" : \"\" %>\n extend T::Sig\n include ActionView::Helpers\n include ApplicationController::HelperMethods\n <% extra_includes.each do |i| %>\n include <%= i %>\n <% end %>\n\n sig { returns(T::Hash[Symbol, T.untyped]) }\n def local_assigns\n # Shim for typechecking\n {}\n end\n\n <%= extra_body %>\n\n <%= locals_sig %>\n def body<%= locals %>\n <% lines.each do |line| %>\n <%= line %>\n <% end %>\n end\nend\n"- VERSION =
'0.4.0'
Class Method Summary collapse
- .class_name_from_path(pathname) ⇒ Object
- .extract_class_name(pathname) ⇒ Object
- .extract_rb_from_erb(input_dir, output_dir) ⇒ Object
- .read_config ⇒ Object
- .requires_defined_locals(file_name) ⇒ Object
- .start(argv) ⇒ Object
Class Method Details
.class_name_from_path(pathname) ⇒ Object
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/sorbet_erb.rb', line 133 def self.class_name_from_path(pathname) # ViewComponents are stored under app/components, and the partials need access to the instance # methods and variables available on the component class, so set the class name to the component # class name. # TODO: support namespacing if pathname.to_s.start_with?('app/components') return [ extract_class_name(pathname).map do |part| ActiveSupport::Inflector.camelize(part) end.join('::'), false ] end # Otherwise make a random class so this doesn't collide with any existing code ["SorbetErb#{SecureRandom.hex(6)}", true] end |
.extract_class_name(pathname) ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
# File 'lib/sorbet_erb.rb', line 151 def self.extract_class_name(pathname) return [] if pathname.to_s == 'app/components' || pathname.to_s == '.' # Strip template suffix basename = File.basename(pathname.basename, '.html.erb') # We need to handle the cases where the dirname matches the component name, or if there's # a namespace # `app/components/my_component/my_component.html.erb` # `app/components/namespace/my_component/my_component.html.erb` dirname = pathname.dirname dirname = dirname.dirname if basename.to_s == dirname.basename.to_s extract_class_name(dirname) + [basename] end |
.extract_rb_from_erb(input_dir, output_dir) ⇒ Object
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/sorbet_erb.rb', line 56 def self.extract_rb_from_erb(input_dir, output_dir) config = read_config input_dirs = if input_dir [input_dir] else config.fetch('input_dirs') end exclude_paths = config.fetch('exclude_paths') output_dir ||= config.fetch('output_dir') skip_missing_locals = config.fetch('skip_missing_locals') puts 'Clearing output directory' FileUtils.rm_rf(output_dir) input_dir_to_paths = input_dirs.flat_map do |d| Dir.glob(File.join(d, '**', '*.erb')).map do |p| [d, p] end end input_dir_to_paths.each do |d, p| pathname = Pathname.new(p) next if exclude_paths.any? { |p| p.include?(pathname.to_s) } extractor = CodeExtractor.new lines, locals, locals_sig = extractor.extract(File.read(p)) # Partials and Turbo streams must use strict locals next if requires_defined_locals(pathname.basename.to_s) && locals.nil? && skip_missing_locals locals ||= '()' locals_sig ||= '' class_name, extend_app_controller = class_name_from_path(pathname) rel_output_dir = File.join( output_dir, pathname.dirname.relative_path_from(d) ) FileUtils.mkdir_p(rel_output_dir) output_path = File.join( rel_output_dir, "#{pathname.basename}.generated.rb" ) erb = ERB.new(ERB_TEMPLATE) File.open(output_path, 'w') do |f| result = erb.result_with_hash( class_name: class_name, extend_app_controller: extend_app_controller, locals: locals, locals_sig: locals_sig, extra_includes: config.fetch('extra_includes'), extra_body: config.fetch('extra_body'), lines: lines ) f.write(result) end end end |
.read_config ⇒ Object
118 119 120 121 122 123 124 125 126 127 |
# File 'lib/sorbet_erb.rb', line 118 def self.read_config path = File.join(Dir.pwd, CONFIG_FILE_NAME) config = if File.exist?(path) Psych.safe_load_file(path) else {} end DEFAULT_CONFIG.merge(config) end |
.requires_defined_locals(file_name) ⇒ Object
129 130 131 |
# File 'lib/sorbet_erb.rb', line 129 def self.requires_defined_locals(file_name) file_name.start_with?('_') || file_name.end_with?('.turbo_stream.erb') end |
.start(argv) ⇒ Object
167 168 169 170 171 172 |
# File 'lib/sorbet_erb.rb', line 167 def self.start(argv) input = argv[0] output = argv[1] SorbetErb.extract_rb_from_erb(input, output) end |