klenod-runtime 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.
- checksums.yaml +7 -0
- data/README.md +30 -0
- data/lib/klenod/runtime/backtrace_rewriter.rb +201 -0
- data/lib/klenod/runtime/bundle.rb +301 -0
- data/lib/klenod/runtime/bundle_format.rb +259 -0
- data/lib/klenod/runtime/mod.rb +112 -0
- data/lib/klenod/runtime/source_map.rb +64 -0
- data/lib/klenod/runtime/version.rb +7 -0
- data/lib/klenod/runtime.rb +52 -0
- metadata +51 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d2479157e269db0089e1ead92345eaf605cbd8e4138977af0950cc8003d42dac
|
|
4
|
+
data.tar.gz: 9b451904ee8f93cc7246098e7ec34991b8b1523239851a5be68f9650b99b7926
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8c40bb3320f1f32a106484d44ffcd51379864f1fa2b0046dc75994c8573e8e754d8b253a9e9e5b3a044c109fe7f3563ad2e9965803e8e446b16bd79eb850c3cf
|
|
7
|
+
data.tar.gz: de76f09e4915a6939becd5a10a51a567e32eedfbe196bf1d9ea039ec6586fd1c88a57555a523c3241156d702045ff70b421cde4081020e8f790a5ac09cdf6a91
|
data/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# klenod-runtime
|
|
2
|
+
|
|
3
|
+
`klenod-runtime` loads and evaluates
|
|
4
|
+
[Klenod](https://github.com/aalin/klenod) bundles in production.
|
|
5
|
+
|
|
6
|
+
It contains only the runtime pieces needed after a bundle has already been built:
|
|
7
|
+
|
|
8
|
+
- `Klenod::Runtime::Bundle`
|
|
9
|
+
- `Klenod::Runtime::Mod`
|
|
10
|
+
- lazy import support
|
|
11
|
+
- source maps and backtrace rewriting
|
|
12
|
+
|
|
13
|
+
This gem should stay free of build plugins and heavyweight build-time dependencies such as RMagick, Syntax Tree, Haml, CSS processing, or file watching.
|
|
14
|
+
|
|
15
|
+
Use this gem in applications that load an existing bundle:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
require "klenod/runtime"
|
|
19
|
+
|
|
20
|
+
bundle = Klenod::Runtime.load_bundle("dist/klenod.bundle")
|
|
21
|
+
exports = bundle.exports("pages/server")
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Production servers can explicitly preload bundled modules:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
bundle.preload_entrypoints
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Build bundles with [klenod-build](https://github.com/aalin/klenod/tree/main/gems/klenod-build).
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Copyright Andrés Alin <andreas.alin@gmail.com>
|
|
5
|
+
# License: AGPL-3.0
|
|
6
|
+
|
|
7
|
+
module Klenod
|
|
8
|
+
module Runtime
|
|
9
|
+
class BacktraceRewriter
|
|
10
|
+
class BacktraceString < String
|
|
11
|
+
attr_reader :parsed_backtrace_entry
|
|
12
|
+
|
|
13
|
+
def initialize(entry)
|
|
14
|
+
super(entry.to_s)
|
|
15
|
+
@parsed_backtrace_entry = entry
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
ParsedBacktraceEntry =
|
|
20
|
+
Data.define(:file, :line, :description) do
|
|
21
|
+
def self.parse(line)
|
|
22
|
+
case line
|
|
23
|
+
in BacktraceString
|
|
24
|
+
line.parsed_backtrace_entry
|
|
25
|
+
in /\A(?<file>.*):(?<line>\d+):in '(?<description>.*)'\z/
|
|
26
|
+
new($~[:file], $~[:line].to_i, $~[:description])
|
|
27
|
+
else
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
"#{file}:#{line}:in '#{description}'"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_backtrace_string
|
|
37
|
+
BacktraceString.new(self)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(mods)
|
|
42
|
+
@source_maps = source_maps_for(mods)
|
|
43
|
+
@source_map_cache = Hash.new { |h, path| h[path] = @source_maps[path] }
|
|
44
|
+
@constant_display_names = constant_display_names_for(mods)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def format_exception(e, source_path: nil)
|
|
48
|
+
reset = "\e[0;48;5;52m"
|
|
49
|
+
rewrite_exception(e)
|
|
50
|
+
sources = format_sources(e.backtrace)
|
|
51
|
+
|
|
52
|
+
[
|
|
53
|
+
"\e[1;31;47m ERROR \e[3;31;47m #{e.class.name}: #{e.message} #{reset}",
|
|
54
|
+
"\e[1;34mBacktrace:#{reset}",
|
|
55
|
+
e
|
|
56
|
+
.backtrace
|
|
57
|
+
.map do |line|
|
|
58
|
+
if line in BacktraceString
|
|
59
|
+
format(
|
|
60
|
+
"#{reset}\e[2mfrom #{reset}\e[1m%<file>s:%<line>d#{reset}\e[2m:in '#{reset}\e[1m%<description>s#{reset}\e[2m'#{reset}",
|
|
61
|
+
line.parsed_backtrace_entry.to_h
|
|
62
|
+
)
|
|
63
|
+
else
|
|
64
|
+
"from #{line}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
.join("\n"),
|
|
68
|
+
formatted_sources(sources, reset)
|
|
69
|
+
].compact.join("\n") + "\e[0m"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def rewrite_exception(e)
|
|
73
|
+
rewrite_exception_message(e)
|
|
74
|
+
e.set_backtrace(rewrite_backtrace(e.backtrace))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def rewrite_backtrace(backtrace)
|
|
78
|
+
backtrace.map do |line|
|
|
79
|
+
if (entry = ParsedBacktraceEntry.parse(line))
|
|
80
|
+
rewrite_backtrace_entry(entry).to_backtrace_string
|
|
81
|
+
else
|
|
82
|
+
line
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def formatted_sources(sources, reset)
|
|
90
|
+
return nil if sources.empty?
|
|
91
|
+
|
|
92
|
+
[
|
|
93
|
+
"\e[1;34mSources:#{reset}",
|
|
94
|
+
sources
|
|
95
|
+
.map do |file, formatted_source|
|
|
96
|
+
"\e[1m#{file}\e[0m\n#{formatted_source}"
|
|
97
|
+
end
|
|
98
|
+
.join("\n")
|
|
99
|
+
].join("\n")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def source_maps_for(mods)
|
|
103
|
+
mods.each_with_object({}) do |(key, mod), index|
|
|
104
|
+
next unless mod.respond_to?(:source_map)
|
|
105
|
+
|
|
106
|
+
source_map = mod.source_map
|
|
107
|
+
next unless source_map
|
|
108
|
+
|
|
109
|
+
index[key.to_s] = source_map
|
|
110
|
+
index[mod.path.to_s] = source_map if mod.respond_to?(:path)
|
|
111
|
+
index[mod.eval_path.to_s] = source_map if mod.respond_to?(:eval_path)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def rewrite_backtrace_entry(entry)
|
|
116
|
+
if (original_line_no = find_original_line_no(entry.file, entry.line))
|
|
117
|
+
entry.with(line: original_line_no, description: rewrite_const_paths(entry.description))
|
|
118
|
+
else
|
|
119
|
+
entry.with(description: rewrite_const_paths(entry.description))
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def constant_display_names_for(mods)
|
|
124
|
+
mods.each_with_object({}) do |(key, mod), index|
|
|
125
|
+
next unless mod.respond_to?(:constant_name)
|
|
126
|
+
|
|
127
|
+
display_path = mod.respond_to?(:path) ? mod.path : key
|
|
128
|
+
index["Klenod::Runtime::Generated::#{mod.constant_name}"] = "Mod[#{display_path.inspect}]"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def rewrite_const_paths(value)
|
|
133
|
+
@constant_display_names.reduce(value.to_s) do |message, (generated_name, display_name)|
|
|
134
|
+
message.gsub(generated_name, display_name)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def rewrite_exception_message(error)
|
|
139
|
+
message = rewrite_const_paths(error.message)
|
|
140
|
+
return if message == error.message
|
|
141
|
+
|
|
142
|
+
error.define_singleton_method(:message) { message }
|
|
143
|
+
error.define_singleton_method(:to_s) { message }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def find_original_line_no(file, line_no)
|
|
147
|
+
@source_map_cache[file]&.find_original_line_no(line_no)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def format_sources(backtrace)
|
|
151
|
+
backtrace
|
|
152
|
+
.select { |line| line.is_a?(BacktraceString) }
|
|
153
|
+
.map(&:parsed_backtrace_entry)
|
|
154
|
+
.group_by(&:file)
|
|
155
|
+
.map do |file, entries|
|
|
156
|
+
if (source_map = @source_map_cache[file])
|
|
157
|
+
[file, format_source(source_map.input, entries.map(&:line))]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
.compact
|
|
161
|
+
.to_h
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def format_source(source, interesting_lines)
|
|
165
|
+
ranges =
|
|
166
|
+
merge_overlapping_ranges(interesting_lines.map { (it - 2)..(it + 2) })
|
|
167
|
+
lines = source.each_line.to_a
|
|
168
|
+
|
|
169
|
+
ranges
|
|
170
|
+
.map do |range|
|
|
171
|
+
range
|
|
172
|
+
.map do |i|
|
|
173
|
+
next if i <= 0
|
|
174
|
+
line = lines[i - 1]
|
|
175
|
+
next unless line
|
|
176
|
+
|
|
177
|
+
str = format("%3d: %s", i, line.chomp)
|
|
178
|
+
interesting_lines.include?(i) ? "\e[1;31m#{str}\e[0m" : str
|
|
179
|
+
end
|
|
180
|
+
.compact
|
|
181
|
+
.join("\n")
|
|
182
|
+
end
|
|
183
|
+
.join("\n\e[37;44m ... \e[0m\n")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def merge_overlapping_ranges(ranges)
|
|
187
|
+
ranges.each_with_object([]) do |range, merged|
|
|
188
|
+
if (idx = merged.find_index { |r| r.overlap?(range) })
|
|
189
|
+
overlapping = merged[idx]
|
|
190
|
+
merged[idx] = [overlapping.begin, range.begin].min..[
|
|
191
|
+
overlapping.end,
|
|
192
|
+
range.end
|
|
193
|
+
].max
|
|
194
|
+
else
|
|
195
|
+
merged << range
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Klenod
|
|
4
|
+
module Runtime
|
|
5
|
+
ModuleSpec =
|
|
6
|
+
Data.define(:id, :source_path, :source, :imports, :source_map, :version, :constant_name)
|
|
7
|
+
|
|
8
|
+
ImportSpec = Data.define(:target_id, :value, :eager)
|
|
9
|
+
DefaultImport = Data.define(:name)
|
|
10
|
+
|
|
11
|
+
AssetSpec =
|
|
12
|
+
Data.define(:logical_name, :content_hash, :output_path, :content_type, :metadata)
|
|
13
|
+
AssetReference = Data.define(:index, :asset)
|
|
14
|
+
|
|
15
|
+
class Bundle
|
|
16
|
+
attr_reader :entrypoints, :modules, :assets, :source_root
|
|
17
|
+
|
|
18
|
+
def self.load(source, source_root: nil)
|
|
19
|
+
BundleFormat.load(source, source_root: source_root)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.load_file(path, source_root: nil)
|
|
23
|
+
load(path, source_root: source_root)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(entrypoints, modules, assets, source_root: nil)
|
|
27
|
+
@entrypoints = entrypoints
|
|
28
|
+
@modules = modules
|
|
29
|
+
@assets = assets
|
|
30
|
+
@source_root = source_root
|
|
31
|
+
@mods = {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def load(entrypoint = nil)
|
|
35
|
+
id =
|
|
36
|
+
if entrypoint
|
|
37
|
+
entrypoint
|
|
38
|
+
elsif entrypoints.respond_to?(:values)
|
|
39
|
+
entrypoints.values.first
|
|
40
|
+
else
|
|
41
|
+
entrypoints.first
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
id = id.to_s
|
|
45
|
+
id = entrypoints.fetch(id, id) if entrypoints.respond_to?(:fetch)
|
|
46
|
+
|
|
47
|
+
instantiate(id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def load_entrypoints
|
|
51
|
+
entrypoint_ids = entrypoints.respond_to?(:values) ? entrypoints.values : entrypoints
|
|
52
|
+
entrypoint_ids.map { |entrypoint| load(entrypoint) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def preload(module_ref = nil)
|
|
56
|
+
module_ids =
|
|
57
|
+
if module_ref
|
|
58
|
+
reachable_module_ids(module_ref)
|
|
59
|
+
else
|
|
60
|
+
@modules.keys
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
module_ids.map { |module_id| instantiate(module_id) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def preload_entrypoints
|
|
67
|
+
entrypoint_ids = entrypoints.respond_to?(:values) ? entrypoints.values : entrypoints
|
|
68
|
+
seen = Set.new
|
|
69
|
+
module_ids =
|
|
70
|
+
entrypoint_ids
|
|
71
|
+
.flat_map { |entrypoint| reachable_module_ids(entrypoint) }
|
|
72
|
+
.select { |module_id| seen.add?(module_id) }
|
|
73
|
+
module_ids.map { |module_id| instantiate(module_id) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def exports(entrypoint = nil)
|
|
77
|
+
load(entrypoint).const_get(:Exports)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def mod(id)
|
|
81
|
+
@mods.fetch(id.to_s)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def asset(output_path)
|
|
85
|
+
@assets.fetch(output_path)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def assets_for(logical_name)
|
|
89
|
+
@assets.values.select { |asset| asset.logical_name == logical_name.to_s }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def assets_for_module(module_ref, type: nil, content_type: nil, recursive: true)
|
|
93
|
+
asset_references_for_module(module_ref, type: type, content_type: content_type, recursive: recursive)
|
|
94
|
+
.map(&:asset)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def asset_references_for_module(module_ref, type: nil, content_type: nil, recursive: true)
|
|
98
|
+
seen_assets = {}
|
|
99
|
+
module_ids_for_assets(module_ref, recursive: recursive)
|
|
100
|
+
.each_with_index
|
|
101
|
+
.flat_map do |module_id, index|
|
|
102
|
+
assets_for_runtime_module(module_id).filter_map do |asset|
|
|
103
|
+
next unless asset_matches?(asset, type: type, content_type: content_type)
|
|
104
|
+
next if seen_assets.key?(asset.output_path)
|
|
105
|
+
|
|
106
|
+
seen_assets[asset.output_path] = true
|
|
107
|
+
AssetReference.new(index:, asset:)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def each_asset(&block)
|
|
113
|
+
return enum_for(:each_asset) unless block
|
|
114
|
+
|
|
115
|
+
@assets.each_value(&block)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def source_root=(source_root)
|
|
119
|
+
@source_root = source_root&.to_s
|
|
120
|
+
@mods = {}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def marshal_dump
|
|
124
|
+
[@entrypoints, @modules, @assets, @source_root]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def marshal_load(data)
|
|
128
|
+
@entrypoints, @modules, @assets, @source_root = data
|
|
129
|
+
@mods = {}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def module_id_for(module_ref)
|
|
133
|
+
id = module_ref.respond_to?(:path) ? module_ref.path : module_ref.to_s
|
|
134
|
+
id = entrypoints.fetch(id, id) if entrypoints.respond_to?(:fetch)
|
|
135
|
+
return id if @modules.key?(id)
|
|
136
|
+
canonical_module_id = module_id_for_canonical_ref(id)
|
|
137
|
+
return canonical_module_id if canonical_module_id
|
|
138
|
+
|
|
139
|
+
if (relative_id = module_id_for_absolute_ref(id))
|
|
140
|
+
return relative_id
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
raise KeyError, "No module in bundle for #{module_ref.inspect}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def assets_for_runtime_module(module_id)
|
|
149
|
+
module_spec = @modules.fetch(module_id)
|
|
150
|
+
logical_names = [module_spec.id, module_spec.source_path].uniq
|
|
151
|
+
@assets.values.select { |asset| logical_names.include?(asset.logical_name) }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
SCHEME_PATTERN = /\A[A-Za-z][A-Za-z0-9+.-]*:/
|
|
155
|
+
|
|
156
|
+
def module_id_for_canonical_ref(id)
|
|
157
|
+
canonical =
|
|
158
|
+
if id.match?(SCHEME_PATTERN)
|
|
159
|
+
canonical_scheme_ref(id)
|
|
160
|
+
elsif !id.start_with?("/")
|
|
161
|
+
"app:/#{id.delete_prefix("/")}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
canonical if canonical && @modules.key?(canonical)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def canonical_scheme_ref(id)
|
|
168
|
+
scheme, rest = id.split(":", 2)
|
|
169
|
+
return id if rest.start_with?("/", "//")
|
|
170
|
+
|
|
171
|
+
"#{scheme}:/#{rest}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def module_id_for_absolute_ref(id)
|
|
175
|
+
return nil unless source_root
|
|
176
|
+
|
|
177
|
+
root = File.expand_path(source_root)
|
|
178
|
+
path = File.expand_path(id)
|
|
179
|
+
return nil unless path.start_with?("#{root}/")
|
|
180
|
+
|
|
181
|
+
relative = path.delete_prefix("#{root}/")
|
|
182
|
+
relative if @modules.key?(relative)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def reachable_module_ids(module_ref)
|
|
186
|
+
root_id = module_id_for(module_ref)
|
|
187
|
+
seen = Set.new
|
|
188
|
+
ordered = []
|
|
189
|
+
queue = [root_id]
|
|
190
|
+
index = 0
|
|
191
|
+
|
|
192
|
+
while index < queue.length
|
|
193
|
+
module_id = queue[index]
|
|
194
|
+
index += 1
|
|
195
|
+
next unless seen.add?(module_id)
|
|
196
|
+
next unless @modules.key?(module_id)
|
|
197
|
+
|
|
198
|
+
ordered << module_id
|
|
199
|
+
queue.concat(
|
|
200
|
+
@modules
|
|
201
|
+
.fetch(module_id)
|
|
202
|
+
.imports
|
|
203
|
+
.values
|
|
204
|
+
.map { |import_spec| import_spec.is_a?(ImportSpec) ? import_spec.target_id : import_spec }
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
ordered
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def module_ids_for_assets(module_ref, recursive:)
|
|
212
|
+
return Array(module_ref).map { |ref| module_id_for(ref) }.uniq unless recursive
|
|
213
|
+
|
|
214
|
+
seen = []
|
|
215
|
+
Array(module_ref).flat_map do |ref|
|
|
216
|
+
ordered_module_ids_for_assets(module_id_for(ref), seen)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def ordered_module_ids_for_assets(module_id, seen)
|
|
221
|
+
return [] if seen.include?(module_id)
|
|
222
|
+
return [] unless @modules.key?(module_id)
|
|
223
|
+
|
|
224
|
+
seen << module_id
|
|
225
|
+
dependency_ids =
|
|
226
|
+
@modules
|
|
227
|
+
.fetch(module_id)
|
|
228
|
+
.imports
|
|
229
|
+
.values
|
|
230
|
+
.map { |import_spec| import_spec.is_a?(ImportSpec) ? import_spec.target_id : import_spec }
|
|
231
|
+
|
|
232
|
+
if File.extname(module_id) == ".css"
|
|
233
|
+
dependency_ids.flat_map { |dependency_id| ordered_module_ids_for_assets(dependency_id, seen) } + [module_id]
|
|
234
|
+
else
|
|
235
|
+
[module_id] + dependency_ids.flat_map { |dependency_id| ordered_module_ids_for_assets(dependency_id, seen) }
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def asset_matches?(asset, type:, content_type:)
|
|
240
|
+
return false if type && asset.metadata[:type] != type
|
|
241
|
+
return false if content_type && asset.content_type != content_type
|
|
242
|
+
|
|
243
|
+
true
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def instantiate(id)
|
|
247
|
+
id = id.to_s
|
|
248
|
+
return @mods.fetch(id) if @mods.key?(id)
|
|
249
|
+
|
|
250
|
+
spec = @modules.fetch(id)
|
|
251
|
+
imports =
|
|
252
|
+
spec.imports.to_h do |dependency_id, target_id|
|
|
253
|
+
import_spec =
|
|
254
|
+
if target_id.is_a?(ImportSpec)
|
|
255
|
+
target_id
|
|
256
|
+
else
|
|
257
|
+
ImportSpec.new(target_id, nil, true)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
value =
|
|
261
|
+
if import_spec.eager
|
|
262
|
+
resolve_import_value(import_spec)
|
|
263
|
+
else
|
|
264
|
+
LazyImport.new { resolve_import_value(import_spec) }
|
|
265
|
+
end
|
|
266
|
+
[dependency_id, value]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
@mods[id] =
|
|
270
|
+
Mod.new(
|
|
271
|
+
spec.id,
|
|
272
|
+
spec.source,
|
|
273
|
+
imports: imports,
|
|
274
|
+
source_map: spec.source_map,
|
|
275
|
+
version: spec.version,
|
|
276
|
+
constant_name: spec.constant_name,
|
|
277
|
+
eval_path: eval_path_for(spec)
|
|
278
|
+
)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def eval_path_for(spec)
|
|
282
|
+
return spec.source_path unless source_root
|
|
283
|
+
|
|
284
|
+
File.join(source_root, spec.source_path)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def resolve_import_value(import_spec)
|
|
288
|
+
exports = instantiate(import_spec.target_id).const_get(:Exports)
|
|
289
|
+
|
|
290
|
+
case import_spec.value
|
|
291
|
+
when DefaultImport
|
|
292
|
+
exports.const_get(import_spec.value.name)
|
|
293
|
+
else
|
|
294
|
+
import_spec.value || exports
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
require_relative "bundle_format"
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "version"
|
|
6
|
+
require_relative "source_map"
|
|
7
|
+
|
|
8
|
+
module Klenod
|
|
9
|
+
module Runtime
|
|
10
|
+
class BundleFormatError < StandardError
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module BundleFormat
|
|
14
|
+
MAGIC = "MODPACK_BUNDLE_V1\n"
|
|
15
|
+
FORMAT_VERSION = 1
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def dump(bundle)
|
|
20
|
+
MAGIC + JSON.generate(payload_for(bundle))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def load(source, source_root: nil)
|
|
24
|
+
load_bytes(read_source(source), source_root: source_root)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def load_bytes(bytes, source_root: nil)
|
|
28
|
+
raise BundleFormatError, "Invalid Klenod bundle header" unless bytes.start_with?(MAGIC)
|
|
29
|
+
|
|
30
|
+
body = bytes.byteslice(MAGIC.bytesize, bytes.bytesize - MAGIC.bytesize)
|
|
31
|
+
payload = JSON.parse(body)
|
|
32
|
+
bundle_from_payload(payload, source_root: source_root)
|
|
33
|
+
rescue JSON::ParserError => error
|
|
34
|
+
raise BundleFormatError, "Invalid Klenod bundle JSON: #{error.message}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def payload_for(bundle)
|
|
38
|
+
{
|
|
39
|
+
"format_version" => FORMAT_VERSION,
|
|
40
|
+
"runtime_version" => Runtime::VERSION,
|
|
41
|
+
"source_root" => encode_value(bundle.source_root),
|
|
42
|
+
"entrypoints" => encode_value(bundle.entrypoints),
|
|
43
|
+
"modules" => encode_modules(bundle.modules),
|
|
44
|
+
"assets" => encode_assets(bundle.assets)
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def bundle_from_payload(payload, source_root: nil)
|
|
49
|
+
validate_payload!(payload)
|
|
50
|
+
|
|
51
|
+
bundle =
|
|
52
|
+
Bundle.new(
|
|
53
|
+
decode_value(payload.fetch("entrypoints")),
|
|
54
|
+
decode_modules(payload.fetch("modules")),
|
|
55
|
+
decode_assets(payload.fetch("assets")),
|
|
56
|
+
source_root: decode_value(payload["source_root"])
|
|
57
|
+
)
|
|
58
|
+
bundle.source_root = source_root if source_root
|
|
59
|
+
bundle
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def read_source(source)
|
|
63
|
+
source.respond_to?(:read) ? source.read : File.binread(source)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_payload!(payload)
|
|
67
|
+
raise BundleFormatError, "Malformed Klenod bundle payload" unless payload.is_a?(Hash)
|
|
68
|
+
|
|
69
|
+
version = payload["format_version"]
|
|
70
|
+
unless version == FORMAT_VERSION
|
|
71
|
+
raise BundleFormatError, "Unsupported Klenod bundle format version: #{version.inspect}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
%w[runtime_version source_root entrypoints modules assets].each do |key|
|
|
75
|
+
raise BundleFormatError, "Malformed Klenod bundle payload: missing #{key}" unless payload.key?(key)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def encode_modules(modules)
|
|
80
|
+
modules.to_h do |id, spec|
|
|
81
|
+
[
|
|
82
|
+
id.to_s,
|
|
83
|
+
{
|
|
84
|
+
"id" => spec.id,
|
|
85
|
+
"source_path" => spec.source_path,
|
|
86
|
+
"source" => spec.source,
|
|
87
|
+
"imports" => encode_imports(spec.imports),
|
|
88
|
+
"source_map" => encode_source_map(spec.source_map),
|
|
89
|
+
"version" => spec.version,
|
|
90
|
+
"constant_name" => spec.constant_name
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def decode_modules(payload)
|
|
97
|
+
expect_hash!(payload, "modules").to_h do |id, spec_payload|
|
|
98
|
+
spec_payload = expect_hash!(spec_payload, "module #{id}")
|
|
99
|
+
[
|
|
100
|
+
id,
|
|
101
|
+
ModuleSpec.new(
|
|
102
|
+
spec_payload.fetch("id"),
|
|
103
|
+
spec_payload.fetch("source_path"),
|
|
104
|
+
spec_payload.fetch("source"),
|
|
105
|
+
decode_imports(spec_payload.fetch("imports")),
|
|
106
|
+
decode_source_map(spec_payload["source_map"], spec_payload.fetch("source")),
|
|
107
|
+
spec_payload.fetch("version"),
|
|
108
|
+
spec_payload.fetch("constant_name")
|
|
109
|
+
)
|
|
110
|
+
]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def encode_imports(imports)
|
|
115
|
+
imports.to_h { |name, import| [name.to_s, encode_import(import)] }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def decode_imports(payload)
|
|
119
|
+
expect_hash!(payload, "imports").to_h { |name, import| [name, decode_import(import)] }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def encode_import(import)
|
|
123
|
+
case import
|
|
124
|
+
when ImportSpec
|
|
125
|
+
{
|
|
126
|
+
"type" => "import_spec",
|
|
127
|
+
"target_id" => import.target_id,
|
|
128
|
+
"value" => encode_value(import.value),
|
|
129
|
+
"eager" => import.eager
|
|
130
|
+
}
|
|
131
|
+
else
|
|
132
|
+
{
|
|
133
|
+
"type" => "module_id",
|
|
134
|
+
"target_id" => import.to_s
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def decode_import(payload)
|
|
140
|
+
payload = expect_hash!(payload, "import")
|
|
141
|
+
case payload.fetch("type")
|
|
142
|
+
when "import_spec"
|
|
143
|
+
ImportSpec.new(payload.fetch("target_id"), decode_value(payload["value"]), payload.fetch("eager"))
|
|
144
|
+
when "module_id"
|
|
145
|
+
payload.fetch("target_id")
|
|
146
|
+
else
|
|
147
|
+
raise BundleFormatError, "Unknown import type: #{payload["type"].inspect}"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def encode_assets(assets)
|
|
152
|
+
assets.to_h do |output_path, asset|
|
|
153
|
+
[
|
|
154
|
+
output_path.to_s,
|
|
155
|
+
{
|
|
156
|
+
"logical_name" => asset.logical_name,
|
|
157
|
+
"content_hash" => asset.content_hash,
|
|
158
|
+
"output_path" => asset.output_path,
|
|
159
|
+
"content_type" => asset.content_type,
|
|
160
|
+
"metadata" => encode_value(asset.metadata)
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def decode_assets(payload)
|
|
167
|
+
expect_hash!(payload, "assets").to_h do |output_path, asset_payload|
|
|
168
|
+
asset_payload = expect_hash!(asset_payload, "asset #{output_path}")
|
|
169
|
+
[
|
|
170
|
+
output_path,
|
|
171
|
+
AssetSpec.new(
|
|
172
|
+
asset_payload.fetch("logical_name"),
|
|
173
|
+
asset_payload.fetch("content_hash"),
|
|
174
|
+
asset_payload.fetch("output_path"),
|
|
175
|
+
asset_payload.fetch("content_type"),
|
|
176
|
+
decode_value(asset_payload.fetch("metadata"))
|
|
177
|
+
)
|
|
178
|
+
]
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def encode_source_map(source_map)
|
|
183
|
+
return nil unless source_map
|
|
184
|
+
|
|
185
|
+
{
|
|
186
|
+
"input" => source_map.input,
|
|
187
|
+
"marks_by_output_line" =>
|
|
188
|
+
source_map.marks_by_output_line.to_h do |line, mark|
|
|
189
|
+
[line.to_s, mark.line]
|
|
190
|
+
end
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def decode_source_map(payload, output)
|
|
195
|
+
return nil unless payload
|
|
196
|
+
|
|
197
|
+
payload = expect_hash!(payload, "source map")
|
|
198
|
+
marks =
|
|
199
|
+
expect_hash!(payload.fetch("marks_by_output_line"), "source map marks").to_h do |line, source_line|
|
|
200
|
+
[line.to_i, SourceMap::Mark.new(source_line)]
|
|
201
|
+
end
|
|
202
|
+
SourceMap::SourceMap.new(payload.fetch("input"), output, marks.freeze)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def encode_value(value)
|
|
206
|
+
case value
|
|
207
|
+
when nil, true, false, String, Integer, Float
|
|
208
|
+
value
|
|
209
|
+
when Symbol
|
|
210
|
+
{"__klenod_type" => "symbol", "value" => value.to_s}
|
|
211
|
+
when Array
|
|
212
|
+
value.map { |item| encode_value(item) }
|
|
213
|
+
when Hash
|
|
214
|
+
{
|
|
215
|
+
"__klenod_type" => "hash",
|
|
216
|
+
"entries" =>
|
|
217
|
+
value.map do |key, hash_value|
|
|
218
|
+
[encode_value(key), encode_value(hash_value)]
|
|
219
|
+
end
|
|
220
|
+
}
|
|
221
|
+
when DefaultImport
|
|
222
|
+
{"__klenod_type" => "default_import", "name" => value.name.to_s}
|
|
223
|
+
else
|
|
224
|
+
raise BundleFormatError, "Cannot encode bundle value: #{value.class}"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def decode_value(value)
|
|
229
|
+
case value
|
|
230
|
+
when nil, true, false, String, Integer, Float
|
|
231
|
+
value
|
|
232
|
+
when Array
|
|
233
|
+
value.map { |item| decode_value(item) }
|
|
234
|
+
when Hash
|
|
235
|
+
case value["__klenod_type"]
|
|
236
|
+
when "symbol"
|
|
237
|
+
value.fetch("value").to_sym
|
|
238
|
+
when "hash"
|
|
239
|
+
value.fetch("entries").to_h do |key, hash_value|
|
|
240
|
+
[decode_value(key), decode_value(hash_value)]
|
|
241
|
+
end
|
|
242
|
+
when "default_import"
|
|
243
|
+
DefaultImport.new(value.fetch("name").to_sym)
|
|
244
|
+
else
|
|
245
|
+
value.to_h { |key, hash_value| [key, decode_value(hash_value)] }
|
|
246
|
+
end
|
|
247
|
+
else
|
|
248
|
+
raise BundleFormatError, "Cannot decode bundle value: #{value.class}"
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def expect_hash!(value, label)
|
|
253
|
+
return value if value.is_a?(Hash)
|
|
254
|
+
|
|
255
|
+
raise BundleFormatError, "Malformed Klenod bundle payload: #{label} must be an object"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Klenod
|
|
6
|
+
module Runtime
|
|
7
|
+
module Generated
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class LazyImport
|
|
11
|
+
def initialize(&loader)
|
|
12
|
+
@loader = loader
|
|
13
|
+
@loaded = false
|
|
14
|
+
@value = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
return @value if @loaded
|
|
19
|
+
|
|
20
|
+
@value = @loader.call
|
|
21
|
+
@loaded = true
|
|
22
|
+
@value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
alias_method :value, :call
|
|
26
|
+
|
|
27
|
+
def loaded?
|
|
28
|
+
@loaded
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reset!
|
|
32
|
+
@loaded = false
|
|
33
|
+
@value = nil
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class Mod < Module
|
|
39
|
+
class Exports < Module
|
|
40
|
+
def initialize(mod, imports)
|
|
41
|
+
@mod = mod
|
|
42
|
+
@imports = imports
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def inspect
|
|
46
|
+
"#{@mod.inspect}::Exports"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def __klenod_import__(dependency_id)
|
|
50
|
+
@imports.fetch(dependency_id)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def __klenod_lazy_import__(dependency_id)
|
|
54
|
+
@imports.fetch(dependency_id)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
attr_reader :path, :source, :source_map, :version, :constant_name, :eval_path
|
|
59
|
+
|
|
60
|
+
def self.constant_name_for(path)
|
|
61
|
+
"Mod_#{Digest::SHA256.hexdigest(path)[0, 24]}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def inspect
|
|
65
|
+
"Mod(#{path.inspect})"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def initialize(path, source, imports: {}, source_map: nil, version: 0, constant_name: nil, eval_path: nil)
|
|
69
|
+
@path = path
|
|
70
|
+
@source = source
|
|
71
|
+
@imports = imports
|
|
72
|
+
@source_map = source_map
|
|
73
|
+
@version = version
|
|
74
|
+
@constant_name = constant_name || self.class.constant_name_for(path)
|
|
75
|
+
@eval_path = eval_path || path
|
|
76
|
+
register_constant
|
|
77
|
+
create_exports
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def marshal_dump
|
|
81
|
+
[@path, @source, @source_map, @version, @constant_name, @eval_path]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def marshal_load(data)
|
|
85
|
+
@path, @source, @source_map, @version, @constant_name, @eval_path = data
|
|
86
|
+
@eval_path ||= @path
|
|
87
|
+
@imports = {}
|
|
88
|
+
register_constant
|
|
89
|
+
create_exports
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def to_s
|
|
93
|
+
"#<#{self.class.name} path=#{@path.inspect}>"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def register_constant
|
|
99
|
+
Generated.__send__(:remove_const, @constant_name) if Generated.const_defined?(@constant_name, false)
|
|
100
|
+
Generated.const_set(@constant_name, self)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def create_exports
|
|
104
|
+
remove_const(:Exports) if const_defined?(:Exports, false)
|
|
105
|
+
|
|
106
|
+
exports = Exports.new(self, @imports)
|
|
107
|
+
exports.module_eval(@source, @eval_path, 1)
|
|
108
|
+
const_set(:Exports, exports)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Klenod
|
|
4
|
+
module Runtime
|
|
5
|
+
module SourceMap
|
|
6
|
+
MARK_PREFIX = "SourceMapMark"
|
|
7
|
+
|
|
8
|
+
Mark = Data.define(:line) do
|
|
9
|
+
def self.parse(value)
|
|
10
|
+
return value if value.is_a?(self)
|
|
11
|
+
return nil unless value
|
|
12
|
+
|
|
13
|
+
if value =~ /\A#{MARK_PREFIX}:(?<line>\d+):?\z/
|
|
14
|
+
new($~[:line].to_i)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_s
|
|
19
|
+
"#{MARK_PREFIX}:#{line}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
SourceMap = Data.define(:input, :output, :marks_by_output_line) do
|
|
24
|
+
def self.parse(input, output)
|
|
25
|
+
marks = {}
|
|
26
|
+
return new(input, output, marks.freeze) unless output.include?(MARK_PREFIX)
|
|
27
|
+
|
|
28
|
+
output.each_line.with_index(1) do |line, line_no|
|
|
29
|
+
if (mark = parse_line_mark(line))
|
|
30
|
+
marks[line_no] = mark
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
new(input, output, marks.freeze)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.parse_line_mark(line)
|
|
38
|
+
start = line.index(MARK_PREFIX)
|
|
39
|
+
return nil unless start
|
|
40
|
+
|
|
41
|
+
index = start + MARK_PREFIX.length
|
|
42
|
+
return nil unless line.getbyte(index) == 58 # :
|
|
43
|
+
|
|
44
|
+
index += 1
|
|
45
|
+
line_start = index
|
|
46
|
+
index += 1 while (byte = line.getbyte(index)) && byte >= 48 && byte <= 57
|
|
47
|
+
return nil if index == line_start
|
|
48
|
+
|
|
49
|
+
Mark.new(line.byteslice(line_start, index - line_start).to_i)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def find_original_line_no(output_line_no)
|
|
53
|
+
# Marks apply until the next mark, so generated code only needs a mark
|
|
54
|
+
# before the expression it came from.
|
|
55
|
+
marks_by_output_line
|
|
56
|
+
.select { |line_no, _mark| line_no <= output_line_no }
|
|
57
|
+
.max_by { |line_no, _mark| line_no }
|
|
58
|
+
&.last
|
|
59
|
+
&.line
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "runtime/version"
|
|
4
|
+
require_relative "runtime/source_map"
|
|
5
|
+
require_relative "runtime/backtrace_rewriter"
|
|
6
|
+
require_relative "runtime/mod"
|
|
7
|
+
require_relative "runtime/bundle"
|
|
8
|
+
require_relative "runtime/bundle_format"
|
|
9
|
+
|
|
10
|
+
module Klenod
|
|
11
|
+
module Runtime
|
|
12
|
+
EXECUTABLE_BUNDLE_MARKER = "\n__END__\n".b.freeze
|
|
13
|
+
|
|
14
|
+
def self.load_bundle(source, source_root: nil)
|
|
15
|
+
Bundle.load(source, source_root: source_root)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.load_bundle_in_box(source, source_root: nil, box: nil)
|
|
19
|
+
unless defined?(Ruby::Box) && Ruby::Box.enabled?
|
|
20
|
+
raise "Ruby::Box is disabled. Set RUBY_BOX=1 environment variable to use Ruby::Box."
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
bytes = source.respond_to?(:read) ? source.read : File.binread(source)
|
|
24
|
+
box = prepare_box(box)
|
|
25
|
+
box::Klenod::Runtime::BundleFormat.load_bytes(bytes, source_root: source_root)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.prepare_box(box = nil)
|
|
29
|
+
unless defined?(Ruby::Box) && Ruby::Box.enabled?
|
|
30
|
+
raise "Ruby::Box is disabled. Set RUBY_BOX=1 environment variable to use Ruby::Box."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
box ||= Ruby::Box.new
|
|
34
|
+
box.require(File.expand_path(__FILE__)) unless runtime_loaded_in_box?(box)
|
|
35
|
+
box
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.runtime_loaded_in_box?(box)
|
|
39
|
+
box.const_defined?(:Klenod, false) &&
|
|
40
|
+
box::Klenod.const_defined?(:Runtime, false)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.load_executable_bundle(path, source_root: nil)
|
|
44
|
+
bytes = File.binread(path)
|
|
45
|
+
marker_index = bytes.index(EXECUTABLE_BUNDLE_MARKER)
|
|
46
|
+
raise ArgumentError, "Missing __END__ marker in executable bundle: #{path}" unless marker_index
|
|
47
|
+
|
|
48
|
+
payload = bytes.byteslice(marker_index + EXECUTABLE_BUNDLE_MARKER.bytesize, bytes.bytesize)
|
|
49
|
+
BundleFormat.load_bytes(payload, source_root: source_root)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: klenod-runtime
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andrés Alin
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Klenod runtime loads serialized Ruby module bundles without build plugins
|
|
13
|
+
or development dependencies.
|
|
14
|
+
email:
|
|
15
|
+
- andreas.alin@gmail.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- README.md
|
|
21
|
+
- lib/klenod/runtime.rb
|
|
22
|
+
- lib/klenod/runtime/backtrace_rewriter.rb
|
|
23
|
+
- lib/klenod/runtime/bundle.rb
|
|
24
|
+
- lib/klenod/runtime/bundle_format.rb
|
|
25
|
+
- lib/klenod/runtime/mod.rb
|
|
26
|
+
- lib/klenod/runtime/source_map.rb
|
|
27
|
+
- lib/klenod/runtime/version.rb
|
|
28
|
+
homepage: https://github.com/aalin/klenod
|
|
29
|
+
licenses:
|
|
30
|
+
- MIT
|
|
31
|
+
metadata:
|
|
32
|
+
homepage_uri: https://github.com/aalin/klenod
|
|
33
|
+
source_code_uri: https://github.com/aalin/klenod/tree/main/gems/klenod-runtime
|
|
34
|
+
rdoc_options: []
|
|
35
|
+
require_paths:
|
|
36
|
+
- lib
|
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: 4.0.6
|
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
requirements: []
|
|
48
|
+
rubygems_version: 4.0.16
|
|
49
|
+
specification_version: 4
|
|
50
|
+
summary: Runtime loader for Klenod bundles.
|
|
51
|
+
test_files: []
|