klenod-build 0.0.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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +29 -0
  3. data/lib/klenod/build/asset.rb +235 -0
  4. data/lib/klenod/build/asset_compression.rb +58 -0
  5. data/lib/klenod/build/asset_generation_queue.rb +50 -0
  6. data/lib/klenod/build/cli/application.rb +108 -0
  7. data/lib/klenod/build/cli.rb +3 -0
  8. data/lib/klenod/build/config.rb +142 -0
  9. data/lib/klenod/build/context.rb +370 -0
  10. data/lib/klenod/build/dependency.rb +21 -0
  11. data/lib/klenod/build/errors.rb +175 -0
  12. data/lib/klenod/build/filesystem_resolver.rb +147 -0
  13. data/lib/klenod/build/graph/invalidator.rb +246 -0
  14. data/lib/klenod/build/graph.rb +864 -0
  15. data/lib/klenod/build/graphviz.rb +282 -0
  16. data/lib/klenod/build/hashing.rb +21 -0
  17. data/lib/klenod/build/invalidation_result.rb +55 -0
  18. data/lib/klenod/build/load_result.rb +7 -0
  19. data/lib/klenod/build/loaded_module.rb +38 -0
  20. data/lib/klenod/build/module_id.rb +100 -0
  21. data/lib/klenod/build/module_record.rb +22 -0
  22. data/lib/klenod/build/plugin.rb +35 -0
  23. data/lib/klenod/build/plugins/class_names_runtime.rb +125 -0
  24. data/lib/klenod/build/plugins/component_defaults.rb +12 -0
  25. data/lib/klenod/build/plugins/data_plugin.rb +117 -0
  26. data/lib/klenod/build/plugins/gem_import_plugin.rb +125 -0
  27. data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.json +17687 -0
  28. data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.rb +105 -0
  29. data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.txt +31 -0
  30. data/lib/klenod/build/plugins/google_fonts_plugin.rb +387 -0
  31. data/lib/klenod/build/plugins/haml_plugin/companions.rb +40 -0
  32. data/lib/klenod/build/plugins/haml_plugin/errors.rb +114 -0
  33. data/lib/klenod/build/plugins/haml_plugin/helper_source.rb +123 -0
  34. data/lib/klenod/build/plugins/haml_plugin/parser.rb +85 -0
  35. data/lib/klenod/build/plugins/haml_plugin/transformer/ruby_builder.rb +1039 -0
  36. data/lib/klenod/build/plugins/haml_plugin/transformer.rb +766 -0
  37. data/lib/klenod/build/plugins/haml_plugin.rb +369 -0
  38. data/lib/klenod/build/plugins/image_plugin.rb +401 -0
  39. data/lib/klenod/build/plugins/intl_plugin.rb +34 -0
  40. data/lib/klenod/build/plugins/markdown_compiler.rb +180 -0
  41. data/lib/klenod/build/plugins/markdown_plugin.rb +161 -0
  42. data/lib/klenod/build/plugins/router_plugin.rb +942 -0
  43. data/lib/klenod/build/plugins/ruby_plugin.rb +34 -0
  44. data/lib/klenod/build/plugins/svg_plugin.rb +197 -0
  45. data/lib/klenod/build/plugins.rb +22 -0
  46. data/lib/klenod/build/profiler.rb +79 -0
  47. data/lib/klenod/build/resolution_error_formatter.rb +42 -0
  48. data/lib/klenod/build/resolver.rb +95 -0
  49. data/lib/klenod/build/ruby_import_rewriter.rb +436 -0
  50. data/lib/klenod/build/source_map/editor.rb +110 -0
  51. data/lib/klenod/build/source_map/map.rb +168 -0
  52. data/lib/klenod/build/source_map/vlq.rb +68 -0
  53. data/lib/klenod/build/source_map.rb +12 -0
  54. data/lib/klenod/build/transform_result.rb +12 -0
  55. data/lib/klenod/build/version.rb +7 -0
  56. data/lib/klenod/build/watched_pattern.rb +11 -0
  57. data/lib/klenod/build/watcher.rb +141 -0
  58. data/lib/klenod/build.rb +15 -0
  59. metadata +310 -0
@@ -0,0 +1,864 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tsort"
4
+ require "async"
5
+
6
+ require "klenod/runtime/mod"
7
+ require "klenod/runtime/bundle"
8
+ require_relative "asset_generation_queue"
9
+ require_relative "errors"
10
+ require_relative "graph/invalidator"
11
+ require_relative "hashing"
12
+ require_relative "invalidation_result"
13
+ require_relative "load_result"
14
+ require_relative "module_id"
15
+ require_relative "module_record"
16
+ require_relative "profiler"
17
+ require_relative "resolver"
18
+ require_relative "transform_result"
19
+ require_relative "watched_pattern"
20
+
21
+ module Klenod
22
+ module Build
23
+ class Graph
24
+ include TSort
25
+
26
+ AsyncResult = Data.define(:value, :error) do
27
+ def self.capture
28
+ new(yield, nil)
29
+ rescue => e
30
+ new(nil, e)
31
+ end
32
+
33
+ def self.unwrap(value)
34
+ return value unless value.is_a?(self)
35
+
36
+ raise value.error if value.error
37
+
38
+ value.value
39
+ end
40
+ end
41
+
42
+ FailedModule = Data.define(:error)
43
+
44
+ InFlightLoad = Struct.new(:result, :condition) do
45
+ def self.create
46
+ new(nil, Async::Condition.new)
47
+ end
48
+
49
+ def wait
50
+ AsyncResult.unwrap(result || condition.wait)
51
+ end
52
+
53
+ def finish(result)
54
+ self.result = result
55
+ condition.signal(result)
56
+ end
57
+ end
58
+
59
+ attr_reader :records, :mods, :asset_generation_queue, :mode, :profiler
60
+ attr_reader :plugins
61
+
62
+ def initialize(
63
+ source_dir:,
64
+ plugins:,
65
+ mode: :development,
66
+ asset_generation_concurrency: AssetGenerationQueue::DEFAULT_CONCURRENCY,
67
+ asset_download_concurrency: AssetGenerationQueue::DEFAULT_DOWNLOAD_CONCURRENCY,
68
+ profiler: nil
69
+ )
70
+ @profiler = profiler || Profiler.new
71
+ @resolver = Resolver.new(source_dir: source_dir, profiler: @profiler)
72
+ @plugins = plugins
73
+ @mode = mode
74
+ @asset_generation_queue =
75
+ AssetGenerationQueue.new(
76
+ concurrency: asset_generation_concurrency,
77
+ download_concurrency: asset_download_concurrency
78
+ )
79
+ @records = {}
80
+ @mods = {}
81
+ @virtual_sources = {}
82
+ @virtual_metadata = {}
83
+ @virtual_owners = {}
84
+ @loading_tasks = {}
85
+ end
86
+
87
+ def load(specifier)
88
+ dependency = Dependency.create(specifier: specifier, importer_id: nil, kind: :entrypoint)
89
+ resolved = resolve_dependency(dependency)
90
+ load_module(resolved.module_id)
91
+ end
92
+
93
+ def collect(specifier)
94
+ dependency = Dependency.create(specifier: specifier, importer_id: nil, kind: :entrypoint)
95
+ resolved = resolve_dependency(dependency)
96
+ collect_module(resolved.module_id)
97
+ end
98
+
99
+ def bundle(entrypoints:)
100
+ loaded_entrypoints =
101
+ @profiler.measure(:entrypoints, count: entrypoints.length) do
102
+ entrypoints.to_h do |entrypoint|
103
+ dependency = Dependency.create(specifier: entrypoint, importer_id: nil, kind: :entrypoint)
104
+ resolved = resolve_dependency(dependency)
105
+ [entrypoint, collect_module(resolved.module_id).id.to_s]
106
+ end
107
+ end
108
+ @profiler.measure(:runtime_dependencies) { collect_all_runtime_dependencies }
109
+
110
+ @profiler.measure(:bundle_specs) do
111
+ Runtime::Bundle.new(
112
+ loaded_entrypoints,
113
+ runtime_module_specs,
114
+ runtime_asset_specs,
115
+ source_root: source_dir.to_s
116
+ )
117
+ end
118
+ end
119
+
120
+ def assets
121
+ @records.values.select { |record| record.status == :loaded }.flat_map(&:assets).to_h { |asset| [asset.output_path, asset] }
122
+ end
123
+
124
+ def asset(output_path)
125
+ assets.fetch(output_path)
126
+ end
127
+
128
+ def exports(record_or_module_id)
129
+ module_id = module_id_for(record_or_module_id)
130
+ evaluate_module(module_id).const_get(:Exports)
131
+ end
132
+
133
+ def evaluated?(record_or_module_id)
134
+ mod = @mods[module_id_for(record_or_module_id)]
135
+ mod && !mod.is_a?(FailedModule)
136
+ end
137
+
138
+ def assets_for(logical_name)
139
+ assets.values.select { |asset| asset.logical_name == logical_name.to_s }
140
+ end
141
+
142
+ def assets_for_module(record_or_module_id, type: nil, content_type: nil, recursive: true)
143
+ asset_references_for_module(record_or_module_id, type: type, content_type: content_type, recursive: recursive)
144
+ .map(&:asset)
145
+ end
146
+
147
+ def asset_references_for_module(record_or_module_id, type: nil, content_type: nil, recursive: true)
148
+ seen_assets = {}
149
+ module_ids_for_assets(record_or_module_id, recursive: recursive, type: type)
150
+ .each_with_index
151
+ .flat_map do |module_id, index|
152
+ module_assets = @records.fetch(module_id).assets
153
+ module_assets.filter_map do |asset|
154
+ next unless asset_matches?(asset, type: type, content_type: content_type)
155
+ next if seen_assets.key?(asset.output_path)
156
+
157
+ seen_assets[asset.output_path] = true
158
+ Runtime::AssetReference.new(index:, asset:)
159
+ end
160
+ end
161
+ end
162
+
163
+ def each_asset(&block)
164
+ return enum_for(:each_asset) unless block
165
+
166
+ assets.each_value(&block)
167
+ end
168
+
169
+ def wait_for_assets
170
+ with_async_task do |task|
171
+ each_asset.map { |asset| task.async { asset.wait } }.each(&:wait)
172
+ end
173
+ end
174
+
175
+ def resolve_dependency(dependency)
176
+ if (virtual_module_id = dependency.metadata[:virtual_module_id])
177
+ @profiler.count(:resolve_virtual_metadata_hit)
178
+ return ResolvedDependency.new(dependency, virtual_module_id, {virtual: true})
179
+ end
180
+
181
+ @plugins.each do |plugin|
182
+ @profiler.count(:plugin_resolve_check)
183
+ resolved =
184
+ @profiler.measure(:plugin_resolve, plugin: plugin.class.name, specifier: dependency.specifier) do
185
+ plugin.resolve(dependency, self)
186
+ end
187
+ @profiler.count(:plugin_resolve_hit) if resolved
188
+ return resolved if resolved
189
+ end
190
+
191
+ @profiler.measure(:resolver_resolve, specifier: dependency.specifier) do
192
+ @resolver.resolve(dependency)
193
+ end
194
+ end
195
+
196
+ def absolute_path(module_id)
197
+ @resolver.absolute_path(module_id)
198
+ end
199
+
200
+ def source_dir
201
+ @resolver.source_dir
202
+ end
203
+
204
+ def register_virtual_module(module_id, source, owner_id: nil, metadata: {})
205
+ @virtual_sources[module_id] = source
206
+ @virtual_metadata[module_id] = metadata.freeze
207
+ @virtual_owners[module_id] = owner_id if owner_id
208
+ end
209
+
210
+ def virtual_module_metadata(module_id)
211
+ @virtual_metadata.fetch(module_id, {})
212
+ end
213
+
214
+ def unregister_virtual_modules(owner_id)
215
+ module_ids = @virtual_owners.filter_map { |module_id, owner| module_id if owner == owner_id }
216
+ module_ids.each do |module_id|
217
+ @virtual_sources.delete(module_id)
218
+ @virtual_metadata.delete(module_id)
219
+ @virtual_owners.delete(module_id)
220
+ @records.delete(module_id)
221
+ @mods.delete(module_id)
222
+ end
223
+ end
224
+
225
+ def invalidate_paths(changed_paths, removed_paths: [])
226
+ Invalidator.new(self, @resolver, source_loader: method(:read_module_source))
227
+ .invalidate_paths(changed_paths, removed_paths: removed_paths)
228
+ end
229
+
230
+ def load_module(module_id, force: false, reevaluate: false)
231
+ raise_import_cycle_if_present(module_id) unless force || reevaluate
232
+
233
+ if !force && !reevaluate
234
+ return @loading_tasks.fetch(module_id).wait if @loading_tasks.key?(module_id)
235
+
236
+ begin
237
+ in_flight_load = InFlightLoad.create
238
+ @loading_tasks[module_id] = in_flight_load
239
+ result = AsyncResult.capture { load_module_now(module_id, force: force, reevaluate: reevaluate) }
240
+ in_flight_load.finish(result)
241
+ return AsyncResult.unwrap(result)
242
+ ensure
243
+ @loading_tasks.delete(module_id) if @loading_tasks[module_id].equal?(in_flight_load)
244
+ end
245
+ end
246
+
247
+ load_module_now(module_id, force: force, reevaluate: reevaluate)
248
+ end
249
+
250
+ def load_module_now(module_id, force: false, reevaluate: false)
251
+ with_loading_stack(module_id) do
252
+ loaded_source = read_module_source(module_id)
253
+ source = loaded_source.source
254
+ source_hash = loaded_source.source_hash || Hashing.hexdigest(source)
255
+ cached = @records[module_id]
256
+
257
+ if cached&.source_hash == source_hash && !force && !reevaluate
258
+ raise_failed_module!(cached)
259
+ @profiler.count(:load_module_cache_hit)
260
+ evaluate_module(module_id) unless @mods.key?(module_id)
261
+ return cached
262
+ end
263
+ @profiler.count(:load_module_cache_miss)
264
+
265
+ transform = loaded_source.transform || transform_module_source(module_id, source)
266
+ resolved_dependencies = resolve_transform_dependencies(module_id, transform)
267
+ dependency_records = load_eager_dependency_records(resolved_dependencies)
268
+ transform = finalize_transform_result(module_id, transform, resolved_dependencies, dependency_records)
269
+ assert_supported_transform!(module_id, source, transform)
270
+ transformed_hash = Hashing.hexdigest(transform.code)
271
+ mod = instantiate_module(module_id, transform, resolved_dependencies, dependency_records, cached)
272
+ record = build_module_record(module_id, source, source_hash, transformed_hash, transform, resolved_dependencies, mod)
273
+
274
+ @records[module_id] = record
275
+ @mods[module_id] = mod
276
+ record
277
+ end
278
+ end
279
+
280
+ def collect_module(module_id, force: false)
281
+ raise_import_cycle_if_present(module_id) unless force
282
+
283
+ if !force
284
+ if @mode == :build && (cached = @records[module_id])
285
+ @profiler.count(:collect_module_cache_hit)
286
+ raise_failed_module!(cached)
287
+ return cached
288
+ end
289
+
290
+ return @loading_tasks.fetch(module_id).wait if @loading_tasks.key?(module_id)
291
+
292
+ begin
293
+ in_flight_load = InFlightLoad.create
294
+ @loading_tasks[module_id] = in_flight_load
295
+ result = AsyncResult.capture { collect_module_now(module_id, force: force) }
296
+ in_flight_load.finish(result)
297
+ return AsyncResult.unwrap(result)
298
+ ensure
299
+ @loading_tasks.delete(module_id) if @loading_tasks[module_id].equal?(in_flight_load)
300
+ end
301
+ end
302
+
303
+ collect_module_now(module_id, force: force)
304
+ end
305
+
306
+ def collect_module_now(module_id, force: false)
307
+ with_loading_stack(module_id) do
308
+ loaded_source = read_module_source(module_id)
309
+ source = loaded_source.source
310
+ source_hash = loaded_source.source_hash || Hashing.hexdigest(source)
311
+ cached = @records[module_id]
312
+
313
+ if cached&.source_hash == source_hash && !force
314
+ raise_failed_module!(cached)
315
+ @profiler.count(:collect_module_cache_hit)
316
+ return cached
317
+ end
318
+ @profiler.count(:collect_module_cache_miss)
319
+
320
+ transform = loaded_source.transform || transform_module_source(module_id, source)
321
+ resolved_dependencies = resolve_transform_dependencies(module_id, transform)
322
+ dependency_records = collect_eager_dependency_records(resolved_dependencies)
323
+ transform = finalize_transform_result(module_id, transform, resolved_dependencies, dependency_records)
324
+ assert_supported_transform!(module_id, source, transform)
325
+ transformed_hash = Hashing.hexdigest(transform.code)
326
+ record = build_module_record(module_id, source, source_hash, transformed_hash, transform, resolved_dependencies, cached)
327
+
328
+ @records[module_id] = record
329
+ @mods.delete(module_id)
330
+ @profiler.progress(:collect_module, module_id: record.id.to_s, total_records: @records.length)
331
+ record
332
+ end
333
+ end
334
+
335
+ def evaluate_module(module_id)
336
+ return @mods.fetch(module_id) if @mods.key?(module_id)
337
+
338
+ record = @records.fetch(module_id) { collect_module(module_id) }
339
+ raise_failed_module!(record)
340
+ evaluate_eager_dependencies(record.resolved_dependencies)
341
+ dependency_records = dependency_records_for(eager_dependencies(record.resolved_dependencies))
342
+ mod =
343
+ Runtime::Mod.new(
344
+ module_id.to_s,
345
+ record.transformed_source,
346
+ imports: imports_for(record.resolved_dependencies, dependency_records),
347
+ source_map: record.source_map,
348
+ version: record.version,
349
+ eval_path: eval_path_for(module_id)
350
+ )
351
+
352
+ @mods[module_id] = mod
353
+ end
354
+
355
+ def tsort_each_node(&block)
356
+ @records.each_key(&block)
357
+ end
358
+
359
+ def tsort_each_child(node, &block)
360
+ record = @records.fetch(node)
361
+ return if record.status != :loaded
362
+
363
+ record.resolved_dependencies.map(&:module_id).each(&block)
364
+ end
365
+
366
+ def module_id_for(record_or_module_id)
367
+ case record_or_module_id
368
+ when ModuleRecord
369
+ record_or_module_id.id
370
+ when ModuleId
371
+ record_or_module_id
372
+ else
373
+ ref = record_or_module_id.to_s
374
+ @records.each_key.find { |module_id| module_id.to_s == ref } ||
375
+ module_id_for_canonical_ref(ref) ||
376
+ module_id_for_absolute_ref(ref) ||
377
+ raise(KeyError, "No module loaded for #{record_or_module_id.inspect}")
378
+ end
379
+ end
380
+
381
+ private
382
+
383
+ def module_id_for_canonical_ref(ref)
384
+ canonical = ModuleId.parse(ref)
385
+ @records.key?(canonical) ? canonical : nil
386
+ rescue
387
+ nil
388
+ end
389
+
390
+ def module_id_for_absolute_ref(ref)
391
+ absolute_path = Pathname.new(ref).expand_path
392
+ relative = absolute_path.relative_path_from(source_dir).to_s
393
+
394
+ @records.each_key.find { |module_id| module_id.path == relative }
395
+ rescue ArgumentError
396
+ nil
397
+ end
398
+
399
+ def reachable_module_ids(record_or_module_id)
400
+ root_id = module_id_for(record_or_module_id)
401
+ seen = Set.new
402
+ queue = [root_id]
403
+
404
+ until queue.empty?
405
+ module_id = queue.shift
406
+ next if seen.include?(module_id)
407
+ next unless @records.key?(module_id)
408
+
409
+ seen << module_id
410
+ record = @records.fetch(module_id)
411
+ queue.concat(record.resolved_dependencies.map(&:module_id)) if record.status == :loaded
412
+ end
413
+
414
+ seen.to_a
415
+ end
416
+
417
+ def module_ids_for_assets(record_or_module_id, recursive:, type: nil)
418
+ return Array(record_or_module_id).map { |module_ref| module_id_for(module_ref) }.uniq unless recursive
419
+
420
+ seen = []
421
+ Array(record_or_module_id).flat_map do |module_ref|
422
+ ordered_module_ids_for_assets(module_id_for(module_ref), seen, type: type)
423
+ end
424
+ end
425
+
426
+ def ordered_module_ids_for_assets(module_id, seen, type: nil)
427
+ return [] if seen.include?(module_id)
428
+ return [] unless @records.key?(module_id)
429
+
430
+ seen << module_id
431
+ record = @records.fetch(module_id)
432
+ dependency_ids =
433
+ record
434
+ .resolved_dependencies
435
+ .reject { skip_asset_dependency?(it, type:) }
436
+ .map(&:module_id)
437
+
438
+ if module_id.extname == ".css"
439
+ dependency_ids.flat_map { |dependency_id| ordered_module_ids_for_assets(dependency_id, seen, type: type) } + [module_id]
440
+ else
441
+ [module_id] + dependency_ids.flat_map { |dependency_id| ordered_module_ids_for_assets(dependency_id, seen, type: type) }
442
+ end
443
+ end
444
+
445
+ def skip_asset_dependency?(resolved_dependency, type:)
446
+ type == :css &&
447
+ resolved_dependency.module_id.extname == ".css" &&
448
+ resolved_dependency.dependency.kind.to_s.start_with?("javascript_")
449
+ end
450
+
451
+ def asset_matches?(asset, type:, content_type:)
452
+ return false if type && asset.metadata[:type] != type
453
+ return false if content_type && asset.content_type != content_type
454
+
455
+ true
456
+ end
457
+
458
+ def loading_stack
459
+ Fiber[:klenod_loading_stack] || []
460
+ end
461
+
462
+ def raise_import_cycle_if_present(module_id)
463
+ stack = loading_stack
464
+ cycle_start = stack.index(module_id)
465
+ return unless cycle_start
466
+
467
+ raise ImportCycleError, stack[cycle_start..] + [module_id]
468
+ end
469
+
470
+ def with_loading_stack(module_id)
471
+ previous_stack = loading_stack
472
+ Fiber[:klenod_loading_stack] = previous_stack + [module_id]
473
+ yield
474
+ ensure
475
+ Fiber[:klenod_loading_stack] = previous_stack
476
+ end
477
+
478
+ def read_module_source(module_id)
479
+ loaded =
480
+ @profiler.measure(:module_source_read, module_id: module_id.to_s) do
481
+ load_source(module_id)
482
+ end
483
+ return loaded if loaded.is_a?(LoadResult)
484
+
485
+ LoadResult.new(loaded, nil, nil)
486
+ end
487
+
488
+ def transform_module_source(module_id, source)
489
+ transform(module_id, source).tap do |result|
490
+ @profiler.count(:transform_dependencies, result.dependencies.length)
491
+ @profiler.count(:transform_assets, result.assets.length)
492
+ end
493
+ end
494
+
495
+ def resolve_transform_dependencies(module_id, transform)
496
+ @profiler.measure(:dependency_resolution, count: transform.dependencies.length) do
497
+ transform.dependencies.map do |dependency|
498
+ resolve_dependency(dependency)
499
+ rescue ResolveError => error
500
+ raise error.with_resolution_context(dependency: dependency, importer_id: module_id)
501
+ end
502
+ end
503
+ end
504
+
505
+ def load_eager_dependency_records(resolved_dependencies)
506
+ load_dependency_records(eager_dependencies(resolved_dependencies))
507
+ end
508
+
509
+ def finalize_transform_result(module_id, transform, resolved_dependencies, dependency_records)
510
+ finalize(module_id, transform, resolved_dependencies, dependency_records)
511
+ end
512
+
513
+ def assert_supported_transform!(module_id, source, transform)
514
+ return if ruby_module_extension?(module_id.extname)
515
+ return unless transform.code == source && transform.dependencies.empty? && transform.assets.empty?
516
+
517
+ raise UnsupportedFileError, "No plugin transformed #{module_id.path.inspect}. Add a plugin for #{module_id.extname.inspect} files."
518
+ end
519
+
520
+ def ruby_module_extension?(extname)
521
+ extname.empty? || extname == ".rb"
522
+ end
523
+
524
+ def instantiate_module(module_id, transform, resolved_dependencies, dependency_records, cached)
525
+ Runtime::Mod.new(
526
+ module_id.to_s,
527
+ transform.code,
528
+ imports: imports_for(resolved_dependencies, dependency_records),
529
+ source_map: transform.source_map,
530
+ version: cached ? cached.version + 1 : 0,
531
+ eval_path: eval_path_for(module_id)
532
+ )
533
+ end
534
+
535
+ def eval_path_for(module_id)
536
+ return module_id.to_s unless module_id.scheme == :app
537
+
538
+ source_dir.join(module_id.path).to_s
539
+ end
540
+
541
+ def imports_for(resolved_dependencies, dependency_records)
542
+ resolved_dependencies.to_h do |resolved_dependency|
543
+ value =
544
+ if resolved_dependency.dependency.eager
545
+ record = dependency_records.fetch(resolved_dependency.dependency.id)
546
+ raise_failed_module!(record)
547
+ import_value(resolved_dependency, record)
548
+ else
549
+ Runtime::LazyImport.new do
550
+ evaluate_module(resolved_dependency.module_id)
551
+ record = @records.fetch(resolved_dependency.module_id)
552
+ raise_failed_module!(record)
553
+ import_value(resolved_dependency, record)
554
+ end
555
+ end
556
+
557
+ [resolved_dependency.dependency.id, value]
558
+ end
559
+ end
560
+
561
+ def evaluate_eager_dependencies(resolved_dependencies)
562
+ eager_dependencies(resolved_dependencies).each do |resolved_dependency|
563
+ evaluate_module(resolved_dependency.module_id)
564
+ end
565
+ end
566
+
567
+ def dependency_records_for(resolved_dependencies)
568
+ resolved_dependencies.to_h do |resolved_dependency|
569
+ [
570
+ resolved_dependency.dependency.id,
571
+ @records.fetch(resolved_dependency.module_id) { collect_module(resolved_dependency.module_id) }
572
+ ]
573
+ end
574
+ end
575
+
576
+ def build_module_record(module_id, source, source_hash, transformed_hash, transform, resolved_dependencies, cached_or_mod)
577
+ version =
578
+ if cached_or_mod.is_a?(Runtime::Mod)
579
+ cached_or_mod.version
580
+ elsif cached_or_mod
581
+ cached_or_mod.version + 1
582
+ else
583
+ 0
584
+ end
585
+
586
+ ModuleRecord.new(
587
+ module_id,
588
+ source_hash,
589
+ transformed_hash,
590
+ transform.dependencies,
591
+ resolved_dependencies,
592
+ source,
593
+ transform.code,
594
+ transform.source_map,
595
+ transform.assets,
596
+ transform.watched_patterns,
597
+ transform.metadata,
598
+ version,
599
+ :loaded
600
+ )
601
+ end
602
+
603
+ def load_dependency_records(resolved_dependencies)
604
+ return {} if resolved_dependencies.empty?
605
+
606
+ if resolved_dependencies.length == 1
607
+ resolved_dependency = resolved_dependencies.fetch(0)
608
+ return {
609
+ resolved_dependency.dependency.id => load_module(resolved_dependency.module_id)
610
+ }
611
+ end
612
+
613
+ with_async_task do |task|
614
+ wait_for_dependency_tasks(
615
+ resolved_dependencies.map do |resolved_dependency|
616
+ [
617
+ resolved_dependency.dependency.id,
618
+ task.async { AsyncResult.capture { load_module(resolved_dependency.module_id) } }
619
+ ]
620
+ end
621
+ )
622
+ end
623
+ end
624
+
625
+ def collect_dependency_records(resolved_dependencies)
626
+ return {} if resolved_dependencies.empty?
627
+
628
+ if resolved_dependencies.length == 1
629
+ resolved_dependency = resolved_dependencies.fetch(0)
630
+ return {
631
+ resolved_dependency.dependency.id => collect_module(resolved_dependency.module_id)
632
+ }
633
+ end
634
+
635
+ with_async_task do |task|
636
+ wait_for_dependency_tasks(
637
+ resolved_dependencies.map do |resolved_dependency|
638
+ [
639
+ resolved_dependency.dependency.id,
640
+ task.async { AsyncResult.capture { collect_module(resolved_dependency.module_id) } }
641
+ ]
642
+ end
643
+ )
644
+ end
645
+ end
646
+
647
+ def wait_for_dependency_tasks(tasks)
648
+ first_error = nil
649
+
650
+ tasks.each_with_object({}) do |(dependency_id, child_task), records|
651
+ records[dependency_id] = AsyncResult.unwrap(child_task.wait)
652
+ rescue => e
653
+ first_error ||= e
654
+ end.tap do
655
+ raise first_error if first_error
656
+ end
657
+ end
658
+
659
+ def raise_failed_module!(record)
660
+ raise record.metadata.fetch(:error) if record&.status == :failed
661
+ end
662
+
663
+ def collect_eager_dependency_records(resolved_dependencies)
664
+ collect_dependency_records(eager_dependencies(resolved_dependencies))
665
+ end
666
+
667
+ def eager_dependencies(resolved_dependencies)
668
+ resolved_dependencies.select { |resolved_dependency| resolved_dependency.dependency.eager }
669
+ end
670
+
671
+ def load_all_runtime_dependencies
672
+ queue = @records.values.flat_map(&:resolved_dependencies)
673
+ seen = Set.new
674
+ index = 0
675
+
676
+ while index < queue.length
677
+ resolved_dependency = queue[index]
678
+ index += 1
679
+ next unless seen.add?(resolved_dependency.module_id)
680
+ next if @records.key?(resolved_dependency.module_id)
681
+
682
+ record = load_module(resolved_dependency.module_id)
683
+ queue.concat(record.resolved_dependencies)
684
+ end
685
+ end
686
+
687
+ def collect_all_runtime_dependencies
688
+ queue =
689
+ @profiler.measure(:runtime_dependencies_seed) do
690
+ @records.values.flat_map(&:resolved_dependencies)
691
+ end
692
+
693
+ seen = Set.new
694
+ index = 0
695
+
696
+ while index < queue.length
697
+ resolved_dependency = @profiler.measure(:runtime_dependencies_dequeue) { queue[index] }
698
+ index += 1
699
+ next unless seen.add?(resolved_dependency.module_id)
700
+ loaded =
701
+ @profiler.measure(:runtime_dependencies_cached_check, module_id: resolved_dependency.module_id.to_s) do
702
+ @records.key?(resolved_dependency.module_id)
703
+ end
704
+ next if loaded
705
+
706
+ record =
707
+ @profiler.measure(:runtime_dependencies_collect_module, module_id: resolved_dependency.module_id.to_s) do
708
+ collect_module(resolved_dependency.module_id)
709
+ end
710
+ @profiler.measure(:runtime_dependencies_enqueue, module_id: record.id.to_s, count: record.resolved_dependencies.length) do
711
+ queue.concat(record.resolved_dependencies)
712
+ end
713
+ end
714
+ end
715
+
716
+ def with_async_task(&block)
717
+ task = current_async_task
718
+
719
+ return block.call(task) if task
720
+
721
+ with_experimental_warnings_suppressed do
722
+ Async { |async_task| AsyncResult.capture { block.call(async_task) } }.wait.then { AsyncResult.unwrap(it) }
723
+ end
724
+ end
725
+
726
+ def current_async_task
727
+ Async::Task.current
728
+ rescue RuntimeError
729
+ nil
730
+ end
731
+
732
+ def with_experimental_warnings_suppressed
733
+ enabled = Warning[:experimental]
734
+ Warning[:experimental] = false
735
+ yield
736
+ ensure
737
+ Warning[:experimental] = enabled
738
+ end
739
+
740
+ def runtime_module_specs
741
+ @records.to_h do |module_id, record|
742
+ imports =
743
+ record.resolved_dependencies.to_h do |resolved_dependency|
744
+ [
745
+ resolved_dependency.dependency.id,
746
+ runtime_import_spec(resolved_dependency, @records.fetch(resolved_dependency.module_id))
747
+ ]
748
+ end
749
+
750
+ [
751
+ module_id.to_s,
752
+ Runtime::ModuleSpec.new(
753
+ module_id.to_s,
754
+ module_id.path,
755
+ record.transformed_source,
756
+ imports,
757
+ record.source_map,
758
+ record.version,
759
+ Runtime::Mod.constant_name_for(module_id.to_s)
760
+ )
761
+ ]
762
+ end
763
+ end
764
+
765
+ def runtime_asset_specs
766
+ @records.values.flat_map(&:assets).to_h do |asset|
767
+ [
768
+ asset.output_path,
769
+ Runtime::AssetSpec.new(
770
+ asset.logical_name,
771
+ asset.content_hash,
772
+ asset.output_path,
773
+ asset.content_type,
774
+ asset.metadata
775
+ )
776
+ ]
777
+ end
778
+ end
779
+
780
+ def runtime_import_spec(resolved_dependency, record)
781
+ value = plugin_runtime_import_value(resolved_dependency, record)
782
+
783
+ Runtime::ImportSpec.new(record.id.to_s, value, resolved_dependency.dependency.eager)
784
+ end
785
+
786
+ def load_source(module_id)
787
+ @plugins.each do |plugin|
788
+ @profiler.count(:plugin_load_check)
789
+ loaded =
790
+ @profiler.measure(:plugin_load, plugin: plugin.class.name, module_id: module_id.to_s) do
791
+ plugin.load(module_id, self)
792
+ end
793
+ @profiler.count(:plugin_load_hit) if loaded
794
+ return loaded if loaded
795
+ end
796
+ if @virtual_sources.key?(module_id)
797
+ @profiler.count(:virtual_source_hit)
798
+ return @virtual_sources.fetch(module_id)
799
+ end
800
+
801
+ @profiler.count(:module_file_read)
802
+ @profiler.measure(:module_file_read, module_id: module_id.to_s) { @resolver.absolute_path(module_id).read(encoding: "UTF-8") }
803
+ end
804
+
805
+ def transform(module_id, source)
806
+ @plugins.reduce(TransformResult.identity(source)) do |current, plugin|
807
+ result =
808
+ @profiler.measure(:plugin_transform, plugin: plugin.class.name, module_id: module_id.to_s) do
809
+ plugin.transform(module_id, current.code, self)
810
+ end || TransformResult.identity(current.code)
811
+ TransformResult.new(
812
+ result.code,
813
+ current.dependencies + result.dependencies,
814
+ result.source_map || current.source_map,
815
+ current.assets + result.assets,
816
+ current.watched_patterns + result.watched_patterns,
817
+ current.metadata.merge(result.metadata)
818
+ )
819
+ end
820
+ end
821
+
822
+ def finalize(module_id, result, resolved_dependencies, dependency_records)
823
+ @plugins.reduce(result) do |current, plugin|
824
+ @profiler.measure(:plugin_finalize, plugin: plugin.class.name, module_id: module_id.to_s) do
825
+ plugin.finalize(module_id, current, resolved_dependencies, dependency_records, self)
826
+ end
827
+ end
828
+ end
829
+
830
+ def import_value(resolved_dependency, record)
831
+ raise_failed_module!(record)
832
+
833
+ value = plugin_import_value(resolved_dependency, record)
834
+ return value unless value.nil?
835
+
836
+ @mods.fetch(record.id).const_get(:Exports)
837
+ end
838
+
839
+ def plugin_import_value(resolved_dependency, record)
840
+ @plugins.each do |plugin|
841
+ value =
842
+ @profiler.measure(:plugin_import_value, plugin: plugin.class.name, module_id: record.id.to_s) do
843
+ plugin.import_value(resolved_dependency, record, self)
844
+ end
845
+ return value unless value.nil?
846
+ end
847
+
848
+ nil
849
+ end
850
+
851
+ def plugin_runtime_import_value(resolved_dependency, record)
852
+ @plugins.each do |plugin|
853
+ value =
854
+ @profiler.measure(:plugin_runtime_import_value, plugin: plugin.class.name, module_id: record.id.to_s) do
855
+ plugin.runtime_import_value(resolved_dependency, record, self)
856
+ end
857
+ return value unless value.nil?
858
+ end
859
+
860
+ nil
861
+ end
862
+ end
863
+ end
864
+ end