homura-runtime 0.3.7 → 0.3.9

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.
@@ -87,38 +87,43 @@ module HomuraRuntime
87
87
  .each do |path|
88
88
  next unless File.read(path).include?("register_async_source")
89
89
 
90
- require_path =
91
- path.sub(Regexp.new("^#{Regexp.escape(lib_dir)}/"), "").sub(
92
- /\.rb\z/,
93
- ""
94
- )
90
+ require_path = path.sub(Regexp.new("^#{Regexp.escape(lib_dir)}/"), "").sub(
91
+ /\.rb\z/,
92
+ ""
93
+ )
95
94
  begin
96
95
  require require_path
96
+
97
97
  loaded += 1
98
98
  if debug
99
- puts "[auto-await] loaded async source from #{spec.name}: #{require_path}"
99
+ puts("[auto-await] loaded async source from #{spec.name}: #{require_path}")
100
100
  end
101
+
101
102
  rescue LoadError, StandardError => e
102
103
  if debug
103
- warn "[auto-await] Warning: failed to load async source from #{spec.name}/#{require_path}: #{e.message}"
104
+ warn(
105
+ "[auto-await] Warning: failed to load async source from #{spec.name}/#{require_path}: #{e.message}"
106
+ )
104
107
  end
105
108
  end
106
109
  end
107
110
  end
108
111
 
109
112
  if debug && loaded.positive?
110
- puts "[auto-await] auto-loaded #{loaded} async source file(s)"
113
+ puts("[auto-await] auto-loaded #{loaded} async source file(s)")
111
114
  end
112
115
  end
113
116
  end
114
117
 
115
- attr_reader :async_classes,
116
- :async_methods,
117
- :async_factories,
118
- :taint_returns,
119
- :async_accessors,
120
- :async_helpers,
121
- :helper_factories
118
+ attr_reader(
119
+ :async_classes,
120
+ :async_methods,
121
+ :async_factories,
122
+ :taint_returns,
123
+ :async_accessors,
124
+ :async_helpers,
125
+ :helper_factories
126
+ )
122
127
 
123
128
  def initialize
124
129
  @async_classes = {}
@@ -135,10 +140,12 @@ module HomuraRuntime
135
140
  methods = @async_methods[class_name]
136
141
  return true if methods&.include?(method_name)
137
142
  except = @async_classes[class_name]
138
- if except && !except.include?(method_name.to_s) &&
139
- !except.include?(method_name.to_sym)
143
+ if except &&
144
+ !except.include?(method_name.to_s) &&
145
+ !except.include?(method_name.to_sym)
140
146
  return true
141
147
  end
148
+
142
149
  false
143
150
  end
144
151
 
@@ -151,8 +158,10 @@ module HomuraRuntime
151
158
  end
152
159
 
153
160
  def tainted_class?(class_name)
154
- @async_classes.key?(class_name) || @async_methods.key?(class_name) ||
155
- @async_factories.key?(class_name) || @taint_returns.key?(class_name)
161
+ @async_classes.key?(class_name) ||
162
+ @async_methods.key?(class_name) ||
163
+ @async_factories.key?(class_name) ||
164
+ @taint_returns.key?(class_name)
156
165
  end
157
166
  end
158
167
  end
@@ -161,100 +170,106 @@ end
161
170
  # Each binding declares which methods return Promises so the
162
171
  # build-time analyzer can insert .__await__ automatically.
163
172
  HomuraRuntime::AsyncRegistry.register_async_source do
164
- async_method "Cloudflare::D1Database", :execute
165
- async_method "Cloudflare::D1Database", :get_first_row
166
- async_method "Cloudflare::D1Database", :execute_insert
167
- async_method "Cloudflare::D1Database", :execute_batch
168
- taint_return "Cloudflare::D1Database", :prepare, "Cloudflare::D1Statement"
169
- taint_return "Cloudflare::D1Database", :[], "Cloudflare::D1Statement"
170
-
171
- async_method "Cloudflare::D1Statement", :all
172
- async_method "Cloudflare::D1Statement", :first
173
- async_method "Cloudflare::D1Statement", :run
173
+ async_method("Cloudflare::D1Database", :execute)
174
+ async_method("Cloudflare::D1Database", :get_first_row)
175
+ async_method("Cloudflare::D1Database", :execute_insert)
176
+ async_method("Cloudflare::D1Database", :execute_batch)
177
+ taint_return("Cloudflare::D1Database", :prepare, "Cloudflare::D1Statement")
178
+ taint_return("Cloudflare::D1Database", :[], "Cloudflare::D1Statement")
179
+
180
+ async_method("Cloudflare::D1Statement", :all)
181
+ async_method("Cloudflare::D1Statement", :first)
182
+ async_method("Cloudflare::D1Statement", :run)
174
183
  # `bind` returns a new D1Statement for further chaining. Tainting the
175
184
  # return preserves the type so the auto-await pass keeps chaining
176
185
  # `.run` / `.all` / `.first` on the bound statement (otherwise
177
186
  # `db.prepare(sql).bind(...).run` drops await on `.run` and
178
187
  # `flatten_meta` ends up receiving a JS Promise instead of the
179
188
  # metadata Hash).
180
- taint_return "Cloudflare::D1Statement", :bind, "Cloudflare::D1Statement"
181
-
182
- async_method "Cloudflare::KVNamespace", :get
183
- async_method "Cloudflare::KVNamespace", :get_with_metadata
184
- async_method "Cloudflare::KVNamespace", :put
185
- async_method "Cloudflare::KVNamespace", :delete
186
- async_method "Cloudflare::KVNamespace", :list
187
-
188
- async_method "Cloudflare::R2Bucket", :get
189
- async_method "Cloudflare::R2Bucket", :get_binary
190
- async_method "Cloudflare::R2Bucket", :put
191
- async_method "Cloudflare::R2Bucket", :delete
192
- async_method "Cloudflare::R2Bucket", :list
193
- async_method "Cloudflare::R2Bucket", :head
194
-
195
- async_method "Cloudflare::AI", :run
196
- async_method "Cloudflare::AI", :speak
197
- async_method "Cloudflare::AI", :speak_data_url
198
- taint_return "Cloudflare::AI", :run_stream, "Cloudflare::AI::Stream"
199
- async_method "Cloudflare::AI::Binding", :run
200
- async_method "Cloudflare::AI::Binding", :chat
201
- async_method "Cloudflare::AI::Binding", :chat_text
202
- async_method "Cloudflare::AI::Binding", :transcribe
203
- async_method "Cloudflare::AI::Binding", :transcribe_text
204
- async_method "Cloudflare::AI::Binding", :speak
205
- async_method "Cloudflare::AI::Binding", :speak_data_url
206
- taint_return "Cloudflare::AI::Binding", :run_stream, "Cloudflare::AI::Stream"
207
-
208
- async_method "Cloudflare::Cache", :match
209
- async_method "Cloudflare::Cache", :put
210
- async_method "Cloudflare::Cache", :delete
211
-
212
- async_factory "Cloudflare::Email", :new
213
- async_method "Cloudflare::Email", :send
214
-
215
- async_method "Cloudflare::Queue", :send
216
- async_method "Cloudflare::Queue", :send_batch
217
-
218
- async_factory "Cloudflare::DurableObjectNamespace", :new
219
- taint_return "Cloudflare::DurableObjectNamespace",
220
- :get,
221
- "Cloudflare::DurableObjectStub"
222
- taint_return "Cloudflare::DurableObjectNamespace",
223
- :get_by_name,
224
- "Cloudflare::DurableObjectStub"
225
- taint_return "Cloudflare::DurableObjectState",
226
- :storage,
227
- "Cloudflare::DurableObjectStorage"
228
- async_method "Cloudflare::DurableObjectStub", :fetch
229
- async_method "Cloudflare::DurableObjectStub", :request
230
- async_method "Cloudflare::DurableObjectStub", :get
231
- async_method "Cloudflare::DurableObjectStub", :post
232
- async_method "Cloudflare::DurableObjectStub", :put
233
- async_method "Cloudflare::DurableObjectStub", :delete
234
-
235
- async_method "Cloudflare::DurableObjectStorage", :get
236
- async_method "Cloudflare::DurableObjectStorage", :put
237
- async_method "Cloudflare::DurableObjectStorage", :delete
238
- async_method "Cloudflare::DurableObjectStorage", :list
239
- async_method "Cloudflare::DurableObjectStorage", :transaction
240
-
241
- async_method "Cloudflare::HTTP", :fetch
242
-
243
- async_method "Faraday::Connection", :get
244
- async_method "Faraday::Connection", :post
245
- async_method "Faraday::Connection", :put
246
- async_method "Faraday::Connection", :delete
247
- async_method "Faraday::Connection", :patch
248
- async_method "Faraday::Connection", :head
249
-
250
- helper_factory :d1, "Cloudflare::D1Database"
251
- helper_factory :db, "Cloudflare::D1Database"
252
- helper_factory :kv, "Cloudflare::KVNamespace"
253
- helper_factory :bucket, "Cloudflare::R2Bucket"
254
- helper_factory :ai, "Cloudflare::AI::Binding"
255
- helper_factory :send_email, "Cloudflare::Email"
256
- helper_factory :jobs_queue, "Cloudflare::Queue"
257
- helper_factory :jobs_dlq, "Cloudflare::Queue"
258
- helper_factory :do_counter, "Cloudflare::DurableObjectNamespace"
259
- helper_factory :durable_object, "Cloudflare::DurableObjectStub"
189
+ taint_return("Cloudflare::D1Statement", :bind, "Cloudflare::D1Statement")
190
+
191
+ async_method("Cloudflare::KVNamespace", :get)
192
+ async_method("Cloudflare::KVNamespace", :get_with_metadata)
193
+ async_method("Cloudflare::KVNamespace", :put)
194
+ async_method("Cloudflare::KVNamespace", :delete)
195
+ async_method("Cloudflare::KVNamespace", :list)
196
+
197
+ async_method("Cloudflare::R2Bucket", :get)
198
+ async_method("Cloudflare::R2Bucket", :get_binary)
199
+ async_method("Cloudflare::R2Bucket", :put)
200
+ async_method("Cloudflare::R2Bucket", :delete)
201
+ async_method("Cloudflare::R2Bucket", :list)
202
+ async_method("Cloudflare::R2Bucket", :head)
203
+
204
+ async_method("Cloudflare::AI", :run)
205
+ async_method("Cloudflare::AI", :speak)
206
+ async_method("Cloudflare::AI", :speak_data_url)
207
+ taint_return("Cloudflare::AI", :run_stream, "Cloudflare::AI::Stream")
208
+ async_method("Cloudflare::AI::Binding", :run)
209
+ async_method("Cloudflare::AI::Binding", :chat)
210
+ async_method("Cloudflare::AI::Binding", :chat_text)
211
+ async_method("Cloudflare::AI::Binding", :transcribe)
212
+ async_method("Cloudflare::AI::Binding", :transcribe_text)
213
+ async_method("Cloudflare::AI::Binding", :speak)
214
+ async_method("Cloudflare::AI::Binding", :speak_data_url)
215
+ taint_return("Cloudflare::AI::Binding", :run_stream, "Cloudflare::AI::Stream")
216
+
217
+ async_method("Cloudflare::Cache", :match)
218
+ async_method("Cloudflare::Cache", :put)
219
+ async_method("Cloudflare::Cache", :delete)
220
+
221
+ async_factory("Cloudflare::Email", :new)
222
+ async_method("Cloudflare::Email", :send)
223
+
224
+ async_method("Cloudflare::Queue", :send)
225
+ async_method("Cloudflare::Queue", :send_batch)
226
+
227
+ async_factory("Cloudflare::DurableObjectNamespace", :new)
228
+ taint_return(
229
+ "Cloudflare::DurableObjectNamespace",
230
+ :get,
231
+ "Cloudflare::DurableObjectStub"
232
+ )
233
+ taint_return(
234
+ "Cloudflare::DurableObjectNamespace",
235
+ :get_by_name,
236
+ "Cloudflare::DurableObjectStub"
237
+ )
238
+ taint_return(
239
+ "Cloudflare::DurableObjectState",
240
+ :storage,
241
+ "Cloudflare::DurableObjectStorage"
242
+ )
243
+ async_method("Cloudflare::DurableObjectStub", :fetch)
244
+ async_method("Cloudflare::DurableObjectStub", :request)
245
+ async_method("Cloudflare::DurableObjectStub", :get)
246
+ async_method("Cloudflare::DurableObjectStub", :post)
247
+ async_method("Cloudflare::DurableObjectStub", :put)
248
+ async_method("Cloudflare::DurableObjectStub", :delete)
249
+
250
+ async_method("Cloudflare::DurableObjectStorage", :get)
251
+ async_method("Cloudflare::DurableObjectStorage", :put)
252
+ async_method("Cloudflare::DurableObjectStorage", :delete)
253
+ async_method("Cloudflare::DurableObjectStorage", :list)
254
+ async_method("Cloudflare::DurableObjectStorage", :transaction)
255
+
256
+ async_method("Cloudflare::HTTP", :fetch)
257
+
258
+ async_method("Faraday::Connection", :get)
259
+ async_method("Faraday::Connection", :post)
260
+ async_method("Faraday::Connection", :put)
261
+ async_method("Faraday::Connection", :delete)
262
+ async_method("Faraday::Connection", :patch)
263
+ async_method("Faraday::Connection", :head)
264
+
265
+ helper_factory(:d1, "Cloudflare::D1Database")
266
+ helper_factory(:db, "Cloudflare::D1Database")
267
+ helper_factory(:kv, "Cloudflare::KVNamespace")
268
+ helper_factory(:bucket, "Cloudflare::R2Bucket")
269
+ helper_factory(:ai, "Cloudflare::AI::Binding")
270
+ helper_factory(:send_email, "Cloudflare::Email")
271
+ helper_factory(:jobs_queue, "Cloudflare::Queue")
272
+ helper_factory(:jobs_dlq, "Cloudflare::Queue")
273
+ helper_factory(:do_counter, "Cloudflare::DurableObjectNamespace")
274
+ helper_factory(:durable_object, "Cloudflare::DurableObjectStub")
260
275
  end
@@ -44,6 +44,7 @@ module HomuraRuntime
44
44
  process_def(node)
45
45
  return
46
46
  end
47
+
47
48
  if node.type == :block
48
49
  process_block(node)
49
50
  return
@@ -55,6 +56,7 @@ module HomuraRuntime
55
56
  node.children.each do |child|
56
57
  process_node(child) if child.is_a?(Parser::AST::Node)
57
58
  end
59
+
58
60
  case node.type
59
61
  when :lvasgn
60
62
  process_lvasgn(node)
@@ -96,7 +98,7 @@ module HomuraRuntime
96
98
  @method_returns[method_name] = return_cls if return_cls
97
99
  body_source = node.loc.expression&.source.to_s
98
100
  if @await_nodes.length > before_awaits ||
99
- body_source.include?(".__await__")
101
+ body_source.include?(".__await__")
100
102
  @async_local_methods << method_name
101
103
  end
102
104
 
@@ -121,12 +123,13 @@ module HomuraRuntime
121
123
  def process_send(node)
122
124
  receiver, method_name = *node
123
125
  if receiver.nil? &&
124
- (factory_cls = @registry.helper_factories[method_name])
126
+ (factory_cls = @registry.helper_factories[method_name])
125
127
  @env[method_name] = factory_cls
126
128
  end
129
+
127
130
  if should_await?(node)
128
131
  @await_nodes << node
129
- debug "await target: #{node.loc.expression.source}"
132
+ debug("await target: #{node.loc.expression.source}")
130
133
  end
131
134
  end
132
135
 
@@ -153,8 +156,10 @@ module HomuraRuntime
153
156
  if @method_returns.key?(method_name)
154
157
  return @method_returns[method_name]
155
158
  end
159
+
156
160
  return @env[method_name] if @env.key?(method_name)
157
161
  end
162
+
158
163
  infer_send_class(node)
159
164
  when :index
160
165
  infer_index_class(node)
@@ -178,16 +183,17 @@ module HomuraRuntime
178
183
  if method_name == :new && receiver&.type == :const
179
184
  return const_path(receiver)
180
185
  end
186
+
181
187
  if receiver
182
188
  if method_name == :[]
183
189
  key_node = node.children[2]
184
190
  if key_node&.type == :str
185
191
  key = key_node.children[0]
186
- mapped =
187
- @registry.async_accessors[[env_name(receiver), key.to_sym]]
192
+ mapped = @registry.async_accessors[[env_name(receiver), key.to_sym]]
188
193
  return mapped if mapped
189
194
  end
190
195
  end
196
+
191
197
  accessor_cls = infer_env_accessor(receiver, method_name)
192
198
  return accessor_cls if accessor_cls
193
199
  recv_cls = infer_class(receiver)
@@ -202,6 +208,7 @@ module HomuraRuntime
202
208
  return @method_returns[method_name]
203
209
  end
204
210
  end
211
+
205
212
  nil
206
213
  end
207
214
 
@@ -209,17 +216,18 @@ module HomuraRuntime
209
216
  return {} unless durable_object_define_call?(call_node)
210
217
  return {} unless args_node&.type == :args
211
218
 
212
- arg_names =
213
- args_node.children.filter_map do |arg|
214
- next unless arg&.type == :arg
215
- arg.children[0]
216
- end
219
+ arg_names = args_node.children.filter_map do |arg|
220
+ next unless arg&.type == :arg
221
+ arg.children[0]
222
+ end
223
+
217
224
  return {} if arg_names.empty?
218
225
 
219
- bindings = { arg_names[0] => "Cloudflare::DurableObjectState" }
220
- bindings[
221
- arg_names[1]
222
- ] = "Cloudflare::DurableObjectRequest" if arg_names.length > 1
226
+ bindings = {arg_names[0] => "Cloudflare::DurableObjectState"}
227
+ if arg_names.length > 1
228
+ bindings[arg_names[1]] = "Cloudflare::DurableObjectRequest"
229
+ end
230
+
223
231
  bindings
224
232
  end
225
233
 
@@ -244,6 +252,7 @@ module HomuraRuntime
244
252
  mapped = @registry.async_accessors[[lvar, method_name.to_sym]]
245
253
  return mapped if mapped
246
254
  end
255
+
247
256
  if receiver_node.type == :send
248
257
  recv, meth = *receiver_node
249
258
  if meth == :[] && env_node?(recv)
@@ -253,6 +262,7 @@ module HomuraRuntime
253
262
  mapped = @registry.async_accessors[[env_name(recv), key.to_sym]]
254
263
  return mapped if mapped
255
264
  end
265
+
256
266
  lvar = env_name(recv)
257
267
  mapped = @registry.async_accessors[[lvar, method_name.to_sym]]
258
268
  return mapped if mapped
@@ -262,6 +272,7 @@ module HomuraRuntime
262
272
  mapped = @registry.async_accessors[[lvar, method_name.to_sym]]
263
273
  return mapped if mapped
264
274
  end
275
+
265
276
  parent_cls = infer_env_accessor(recv, meth) if recv&.type == :send
266
277
  return parent_cls if parent_cls
267
278
  elsif receiver_node.type == :lvar
@@ -269,6 +280,7 @@ module HomuraRuntime
269
280
  mapped = @registry.async_accessors[[lvar, method_name.to_sym]]
270
281
  return mapped if mapped
271
282
  end
283
+
272
284
  nil
273
285
  end
274
286
 
@@ -288,11 +300,12 @@ module HomuraRuntime
288
300
  parts.unshift(n.children[1])
289
301
  n = n.children[0]
290
302
  end
303
+
291
304
  parts.join("::")
292
305
  end
293
306
 
294
307
  def debug(msg)
295
- puts "[auto-await] #{msg}" if @debug
308
+ puts("[auto-await] #{msg}") if @debug
296
309
  end
297
310
  end
298
311
  end
@@ -13,6 +13,7 @@ module HomuraRuntime
13
13
  next if already_awaited?(buffer, range)
14
14
  rewriter.replace(range, "#{range.source}.__await__")
15
15
  end
16
+
16
17
  rewriter.process
17
18
  end
18
19
 
@@ -8,6 +8,11 @@ module HomuraRuntime
8
8
  RUNTIME_GEM_NAME = "homura-runtime"
9
9
  SINATRA_GEM_NAME = "sinatra-homura"
10
10
  SEQUEL_D1_GEM_NAME = "sequel-d1"
11
+ SUPPORTED_OPAL_ENTRY_GEMS = %w[phlex literal].freeze
12
+ OPAL_ENTRY_COMPAT_REQUIRES = {
13
+ "phlex" => "phlex/opal_compat",
14
+ "literal" => "literal/opal_compat"
15
+ }.freeze
11
16
 
12
17
  class << self
13
18
  def loaded_spec(name, loaded_specs: Gem.loaded_specs)
@@ -66,7 +71,8 @@ module HomuraRuntime
66
71
  runtime_root(
67
72
  current_file: current_file,
68
73
  loaded_specs: loaded_specs
69
- ).join("runtime", *names)
74
+ )
75
+ .join("runtime", *names)
70
76
  end
71
77
 
72
78
  def ensure_standalone_runtime(
@@ -137,8 +143,7 @@ module HomuraRuntime
137
143
  # app code.
138
144
  opal_gem_paths(root, loaded_specs: loaded_specs).each do |gem_path|
139
145
  basename = gem_path.basename.to_s
140
- rewritten_lib =
141
- root.join("build", "auto_await", "gem_#{basename}", "lib")
146
+ rewritten_lib = root.join("build", "auto_await", "gem_#{basename}", "lib")
142
147
  load_paths << rewritten_lib.to_s if rewritten_lib.directory?
143
148
  %w[lib vendor].each do |sub|
144
149
  dir = gem_path.join(sub)
@@ -165,12 +170,9 @@ module HomuraRuntime
165
170
  return unless gf.file?
166
171
 
167
172
  txt = gf.read
168
- unless (
169
- m =
170
- txt.match(
171
- /#{Regexp.escape(RUNTIME_GEM_NAME)}['"]\s*,\s*path:\s*['"]([^'"]+)['"]/
172
- )
173
- )
173
+ unless (m = txt.match(
174
+ /#{Regexp.escape(RUNTIME_GEM_NAME)}['"]\s*,\s*path:\s*['"]([^'"]+)['"]/
175
+ ))
174
176
  return
175
177
  end
176
178
 
@@ -190,6 +192,10 @@ module HomuraRuntime
190
192
  out = []
191
193
  out.concat(path_gemfile_entries(project_root))
192
194
 
195
+ entry_specs = opal_entry_gem_specs(loaded_specs: loaded_specs)
196
+ out.concat(entry_specs.map { |spec| Pathname(spec.full_gem_path) if spec.full_gem_path }.compact)
197
+ out.concat(opal_dependency_paths(entry_specs, loaded_specs: loaded_specs))
198
+
193
199
  loaded_specs.each_value do |spec|
194
200
  next if wired.include?(spec.name)
195
201
  meta = spec.metadata
@@ -204,6 +210,78 @@ module HomuraRuntime
204
210
  out.uniq
205
211
  end
206
212
 
213
+ def opal_entry_gem_specs(loaded_specs: Gem.loaded_specs)
214
+ names = SUPPORTED_OPAL_ENTRY_GEMS + env_opal_gem_names
215
+ names.uniq.filter_map { |name| loaded_specs[name] }
216
+ end
217
+
218
+ def opal_dependency_paths(specs, loaded_specs: Gem.loaded_specs)
219
+ seen = {}
220
+ out = []
221
+ queue = specs.flat_map { |spec| spec.runtime_dependencies.map(&:name) }
222
+
223
+ until queue.empty?
224
+ name = queue.shift
225
+ next if seen[name]
226
+ seen[name] = true
227
+
228
+ spec = loaded_specs[name]
229
+ next unless spec&.full_gem_path
230
+
231
+ path = Pathname(spec.full_gem_path)
232
+ out << path if path.directory?
233
+ queue.concat(spec.runtime_dependencies.map(&:name))
234
+ end
235
+
236
+ out
237
+ end
238
+
239
+ def write_opal_gems_prelude(project_root, loaded_specs: Gem.loaded_specs)
240
+ entry_specs = opal_entry_gem_specs(loaded_specs: loaded_specs)
241
+ return nil if entry_specs.empty?
242
+
243
+ root = Pathname(project_root)
244
+ out = root.join("build", "homura_opal_gems.rb")
245
+ lines = ["# frozen_string_literal: true", ""]
246
+ entry_specs.each do |spec|
247
+ next unless spec.full_gem_path
248
+
249
+ lib = Pathname(spec.full_gem_path).join("lib")
250
+ next unless lib.directory?
251
+
252
+ lines << "require_tree #{lib.to_s.inspect}, autoload: true"
253
+ end
254
+
255
+ lines << ""
256
+ lines << "require \"zeitwerk/opal_compat\""
257
+ entry_specs.each do |spec|
258
+ compat = OPAL_ENTRY_COMPAT_REQUIRES[spec.name]
259
+ next unless compat && spec.full_gem_path
260
+
261
+ lib = Pathname(spec.full_gem_path).join("lib")
262
+ root_file = lib.join("#{spec.name}.rb")
263
+ next unless root_file.file?
264
+
265
+ root_require = root_file.to_s.delete_suffix(".rb")
266
+ lines << "Zeitwerk.__homura_next_gem_root = #{root_file.to_s.inspect}"
267
+ lines << "require #{root_require.inspect}"
268
+ lines << "`Opal.loaded([#{spec.name.inspect}])`"
269
+ lines << "require #{compat.inspect}"
270
+ end
271
+
272
+ FileUtils.mkdir_p(out.dirname)
273
+ File.write(out, lines.join("\n") << "\n")
274
+ out
275
+ end
276
+
277
+ def env_opal_gem_names(env = ENV)
278
+ env
279
+ .fetch("HOMURA_OPAL_GEMS", "")
280
+ .split(/[\s,]+/)
281
+ .map(&:strip)
282
+ .reject(&:empty?)
283
+ end
284
+
207
285
  # Returns absolute Pathnames for every `path:`-declared gem in the
208
286
  # project's Gemfile that should ship in the Workers bundle.
209
287
  #
@@ -235,11 +313,11 @@ module HomuraRuntime
235
313
  next if stripped.empty? || stripped.start_with?("#")
236
314
 
237
315
  if (m = stripped.match(/\Agroup\s+(.+?)\s+do\b/))
238
- groups =
239
- m[1].scan(/[:'"]([A-Za-z0-9_]+)['"]?/).flatten.map(&:to_sym)
316
+ groups = m[1].scan(/[:'"]([A-Za-z0-9_]+)['"]?/).flatten.map(&:to_sym)
240
317
  group_stack.push(groups)
241
318
  next
242
319
  end
320
+
243
321
  if stripped == "end"
244
322
  group_stack.pop unless group_stack.empty?
245
323
  next
@@ -256,6 +334,7 @@ module HomuraRuntime
256
334
  gem_path = Pathname.new(rel).expand_path(project_root)
257
335
  out << gem_path if gem_path.directory?
258
336
  end
337
+
259
338
  out.uniq
260
339
  end
261
340
  end