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.
- checksums.yaml +7 -0
- data/README.md +29 -0
- data/lib/klenod/build/asset.rb +235 -0
- data/lib/klenod/build/asset_compression.rb +58 -0
- data/lib/klenod/build/asset_generation_queue.rb +50 -0
- data/lib/klenod/build/cli/application.rb +108 -0
- data/lib/klenod/build/cli.rb +3 -0
- data/lib/klenod/build/config.rb +142 -0
- data/lib/klenod/build/context.rb +370 -0
- data/lib/klenod/build/dependency.rb +21 -0
- data/lib/klenod/build/errors.rb +175 -0
- data/lib/klenod/build/filesystem_resolver.rb +147 -0
- data/lib/klenod/build/graph/invalidator.rb +246 -0
- data/lib/klenod/build/graph.rb +864 -0
- data/lib/klenod/build/graphviz.rb +282 -0
- data/lib/klenod/build/hashing.rb +21 -0
- data/lib/klenod/build/invalidation_result.rb +55 -0
- data/lib/klenod/build/load_result.rb +7 -0
- data/lib/klenod/build/loaded_module.rb +38 -0
- data/lib/klenod/build/module_id.rb +100 -0
- data/lib/klenod/build/module_record.rb +22 -0
- data/lib/klenod/build/plugin.rb +35 -0
- data/lib/klenod/build/plugins/class_names_runtime.rb +125 -0
- data/lib/klenod/build/plugins/component_defaults.rb +12 -0
- data/lib/klenod/build/plugins/data_plugin.rb +117 -0
- data/lib/klenod/build/plugins/gem_import_plugin.rb +125 -0
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.json +17687 -0
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.rb +105 -0
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.txt +31 -0
- data/lib/klenod/build/plugins/google_fonts_plugin.rb +387 -0
- data/lib/klenod/build/plugins/haml_plugin/companions.rb +40 -0
- data/lib/klenod/build/plugins/haml_plugin/errors.rb +114 -0
- data/lib/klenod/build/plugins/haml_plugin/helper_source.rb +123 -0
- data/lib/klenod/build/plugins/haml_plugin/parser.rb +85 -0
- data/lib/klenod/build/plugins/haml_plugin/transformer/ruby_builder.rb +1039 -0
- data/lib/klenod/build/plugins/haml_plugin/transformer.rb +766 -0
- data/lib/klenod/build/plugins/haml_plugin.rb +369 -0
- data/lib/klenod/build/plugins/image_plugin.rb +401 -0
- data/lib/klenod/build/plugins/intl_plugin.rb +34 -0
- data/lib/klenod/build/plugins/markdown_compiler.rb +180 -0
- data/lib/klenod/build/plugins/markdown_plugin.rb +161 -0
- data/lib/klenod/build/plugins/router_plugin.rb +942 -0
- data/lib/klenod/build/plugins/ruby_plugin.rb +34 -0
- data/lib/klenod/build/plugins/svg_plugin.rb +197 -0
- data/lib/klenod/build/plugins.rb +22 -0
- data/lib/klenod/build/profiler.rb +79 -0
- data/lib/klenod/build/resolution_error_formatter.rb +42 -0
- data/lib/klenod/build/resolver.rb +95 -0
- data/lib/klenod/build/ruby_import_rewriter.rb +436 -0
- data/lib/klenod/build/source_map/editor.rb +110 -0
- data/lib/klenod/build/source_map/map.rb +168 -0
- data/lib/klenod/build/source_map/vlq.rb +68 -0
- data/lib/klenod/build/source_map.rb +12 -0
- data/lib/klenod/build/transform_result.rb +12 -0
- data/lib/klenod/build/version.rb +7 -0
- data/lib/klenod/build/watched_pattern.rb +11 -0
- data/lib/klenod/build/watcher.rb +141 -0
- data/lib/klenod/build.rb +15 -0
- metadata +310 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ripper"
|
|
4
|
+
require "syntax_tree"
|
|
5
|
+
|
|
6
|
+
require_relative "dependency"
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
require_relative "watched_pattern"
|
|
9
|
+
|
|
10
|
+
module Klenod
|
|
11
|
+
module Build
|
|
12
|
+
class RubyImportRewriter
|
|
13
|
+
ImportCall =
|
|
14
|
+
Data.define(:specifier, :location, :dynamic, :method_name, :eager_override) do
|
|
15
|
+
def eager
|
|
16
|
+
eager_override.nil? ? RubyImportRewriter::IMPORT_METHODS.fetch(method_name).fetch(:eager) : eager_override
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def replacement
|
|
20
|
+
RubyImportRewriter::IMPORT_METHODS.fetch(method_name).fetch(:replacement)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
Result = Data.define(:code, :dependencies, :watched_patterns)
|
|
24
|
+
|
|
25
|
+
IMPORT_METHODS = {
|
|
26
|
+
"import" => {
|
|
27
|
+
replacement: "__klenod_import__",
|
|
28
|
+
eager: true
|
|
29
|
+
},
|
|
30
|
+
"lazy_import" => {
|
|
31
|
+
replacement: "__klenod_lazy_import__",
|
|
32
|
+
eager: false
|
|
33
|
+
},
|
|
34
|
+
"import_glob" => {
|
|
35
|
+
replacement: nil,
|
|
36
|
+
eager: true
|
|
37
|
+
}
|
|
38
|
+
}.freeze
|
|
39
|
+
IMPORT_SOURCE_PATTERN = /(?<![A-Za-z0-9_])(?:import|lazy_import|import_glob)\s*(?:\(|["'])/
|
|
40
|
+
FAST_LITERAL_IMPORT_PATTERN = /(?<![A-Za-z0-9_])(import|lazy_import)\s*\(\s*("(?:\\.|[^"\\#])*")\s*\)/
|
|
41
|
+
|
|
42
|
+
def initialize(module_id:, kind:, source_dir: nil, profiler: nil, dependency_id_offset: 0, source_line_offset: 0, source_column_offset: 0)
|
|
43
|
+
@module_id = module_id
|
|
44
|
+
@kind = kind
|
|
45
|
+
@source_dir = source_dir && Pathname.new(source_dir).expand_path
|
|
46
|
+
@profiler = profiler
|
|
47
|
+
@dependency_id_offset = dependency_id_offset
|
|
48
|
+
@source_line_offset = source_line_offset
|
|
49
|
+
@source_column_offset = source_column_offset
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def rewrite(code)
|
|
53
|
+
return Result.new(code, [], []) unless import_source?(code)
|
|
54
|
+
fast_result = rewrite_literal_import_calls(code)
|
|
55
|
+
return fast_result if fast_result
|
|
56
|
+
|
|
57
|
+
ast = measure(:ruby_import_parse) { SyntaxTree.parse(code) }
|
|
58
|
+
calls = measure(:ruby_import_scan) { import_calls(ast) }
|
|
59
|
+
expanded = expand_import_calls(calls)
|
|
60
|
+
|
|
61
|
+
rewritten = measure(:ruby_import_rewrite_source) { rewrite_import_calls(code, expanded) }
|
|
62
|
+
Result.new(
|
|
63
|
+
rewritten,
|
|
64
|
+
expanded.flat_map(&:dependencies),
|
|
65
|
+
expanded.flat_map(&:watched_patterns)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
ExpandedImport = Data.define(:call, :dependencies, :replacement_source, :watched_patterns)
|
|
72
|
+
|
|
73
|
+
def import_source?(code)
|
|
74
|
+
code.match?(IMPORT_SOURCE_PATTERN)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def rewrite_literal_import_calls(code)
|
|
78
|
+
matches = []
|
|
79
|
+
code.to_enum(:scan, IMPORT_SOURCE_PATTERN).each do
|
|
80
|
+
start = Regexp.last_match.begin(0)
|
|
81
|
+
match = FAST_LITERAL_IMPORT_PATTERN.match(code, start)
|
|
82
|
+
return nil unless match && match.begin(0) == start
|
|
83
|
+
|
|
84
|
+
matches << match
|
|
85
|
+
end
|
|
86
|
+
return nil if matches.empty?
|
|
87
|
+
return nil unless bare_import_tokens?(code, matches)
|
|
88
|
+
|
|
89
|
+
dependencies =
|
|
90
|
+
matches.each_with_index.map do |match, index|
|
|
91
|
+
specifier =
|
|
92
|
+
begin
|
|
93
|
+
match[2].undump
|
|
94
|
+
rescue RuntimeError
|
|
95
|
+
return nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
Dependency
|
|
99
|
+
.create(
|
|
100
|
+
specifier: specifier,
|
|
101
|
+
importer_id: @module_id,
|
|
102
|
+
kind: @kind,
|
|
103
|
+
loc: source_location_for_offset(code, match.begin(0))
|
|
104
|
+
)
|
|
105
|
+
.with(eager: IMPORT_METHODS.fetch(match[1]).fetch(:eager))
|
|
106
|
+
.with(id: "#{@module_id}:dependency:#{@dependency_id_offset + index}")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
rewritten =
|
|
110
|
+
matches
|
|
111
|
+
.zip(dependencies)
|
|
112
|
+
.reverse_each
|
|
113
|
+
.each_with_object(code.dup) do |(match, dependency), source|
|
|
114
|
+
replacement = IMPORT_METHODS.fetch(match[1]).fetch(:replacement)
|
|
115
|
+
source[match.begin(0)...match.end(0)] = "#{replacement}(#{dependency.id.inspect})"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Result.new(rewritten, dependencies, [])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def bare_import_tokens?(code, matches)
|
|
122
|
+
match_starts = matches.to_h { |match| [match.begin(1), match[1]] }
|
|
123
|
+
return true if match_starts.empty?
|
|
124
|
+
|
|
125
|
+
line_offsets = line_offsets_for(code)
|
|
126
|
+
previous_significant_token = nil
|
|
127
|
+
matched = {}
|
|
128
|
+
|
|
129
|
+
Ripper.lex(code).each do |(line, column), type, token, _state|
|
|
130
|
+
offset = line_offsets.fetch(line - 1) + column
|
|
131
|
+
expected = match_starts[offset]
|
|
132
|
+
if expected
|
|
133
|
+
return false unless type == :on_ident && token == expected
|
|
134
|
+
return false if receiver_token?(previous_significant_token)
|
|
135
|
+
|
|
136
|
+
matched[offset] = true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
previous_significant_token = [type, token] unless insignificant_token?(type)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
matched.length == match_starts.length
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def receiver_token?(token)
|
|
146
|
+
token == [:on_period, "."] || token == [:on_op, "::"] || token == [:on_op, "&."]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def line_offsets_for(code)
|
|
150
|
+
offsets = [0]
|
|
151
|
+
code.each_line(chomp: false) { |line| offsets << offsets.last + line.length }
|
|
152
|
+
offsets
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def insignificant_token?(type)
|
|
156
|
+
type == :on_sp || type == :on_ignored_nl || type == :on_nl || type == :on_comment
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def measure(name, &block)
|
|
160
|
+
return yield unless @profiler
|
|
161
|
+
|
|
162
|
+
@profiler.measure(name, module_id: @module_id.to_s, kind: @kind, &block)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def import_calls(node)
|
|
166
|
+
calls = []
|
|
167
|
+
walk(node) do |child|
|
|
168
|
+
if child.instance_of?(::SyntaxTree::CallNode)
|
|
169
|
+
next unless child.instance_variable_get(:@receiver).nil?
|
|
170
|
+
next unless IMPORT_METHODS.key?(child.instance_variable_get(:@message)&.value)
|
|
171
|
+
|
|
172
|
+
calls << build_import_call(child)
|
|
173
|
+
elsif child.instance_of?(::SyntaxTree::Command)
|
|
174
|
+
next unless IMPORT_METHODS.key?(child.instance_variable_get(:@message)&.value)
|
|
175
|
+
|
|
176
|
+
calls << build_import_command(child)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
calls
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def build_import_call(node)
|
|
183
|
+
arguments = node.instance_variable_get(:@arguments)
|
|
184
|
+
parts = arguments&.instance_variable_get(:@arguments)&.instance_variable_get(:@parts) || []
|
|
185
|
+
build_import_from_parts(node, parts)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def build_import_command(node)
|
|
189
|
+
arguments = node.instance_variable_get(:@arguments)
|
|
190
|
+
parts = arguments&.instance_variable_get(:@parts) || []
|
|
191
|
+
build_import_from_parts(node, parts)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def build_import_from_parts(node, parts)
|
|
195
|
+
return build_import_glob_from_parts(node, parts) if node.instance_variable_get(:@message).value == "import_glob"
|
|
196
|
+
|
|
197
|
+
first = unwrap_paren(parts.first)
|
|
198
|
+
string_parts = first&.instance_variable_get(:@parts)
|
|
199
|
+
literal =
|
|
200
|
+
(parts.length == 1) &&
|
|
201
|
+
first&.class&.name == "SyntaxTree::StringLiteral" &&
|
|
202
|
+
string_parts&.length == 1 &&
|
|
203
|
+
string_parts.first.instance_of?(::SyntaxTree::TStringContent)
|
|
204
|
+
|
|
205
|
+
ImportCall.new(
|
|
206
|
+
literal ? string_parts.first.value : nil,
|
|
207
|
+
node.instance_variable_get(:@location),
|
|
208
|
+
!literal,
|
|
209
|
+
node.instance_variable_get(:@message).value,
|
|
210
|
+
nil
|
|
211
|
+
)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def build_import_glob_from_parts(node, parts)
|
|
215
|
+
first = unwrap_paren(parts.first)
|
|
216
|
+
string_parts = first&.instance_variable_get(:@parts)
|
|
217
|
+
literal =
|
|
218
|
+
first&.class&.name == "SyntaxTree::StringLiteral" &&
|
|
219
|
+
string_parts&.length == 1 &&
|
|
220
|
+
string_parts.first.instance_of?(::SyntaxTree::TStringContent)
|
|
221
|
+
|
|
222
|
+
eager = true
|
|
223
|
+
valid_options = true
|
|
224
|
+
if parts.length == 2
|
|
225
|
+
eager = eager_option_value(parts.fetch(1))
|
|
226
|
+
valid_options = !eager.nil?
|
|
227
|
+
elsif parts.length != 1
|
|
228
|
+
valid_options = false
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
ImportCall.new(
|
|
232
|
+
(literal && valid_options) ? string_parts.first.value : nil,
|
|
233
|
+
node.instance_variable_get(:@location),
|
|
234
|
+
!literal || !valid_options,
|
|
235
|
+
node.instance_variable_get(:@message).value,
|
|
236
|
+
eager
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def eager_option_value(node)
|
|
241
|
+
return unless node.instance_of?(::SyntaxTree::BareAssocHash)
|
|
242
|
+
|
|
243
|
+
assocs = node.instance_variable_get(:@assocs)
|
|
244
|
+
return unless assocs.length == 1
|
|
245
|
+
|
|
246
|
+
assoc = assocs.fetch(0)
|
|
247
|
+
key = assoc.instance_variable_get(:@key)
|
|
248
|
+
return unless key.instance_of?(::SyntaxTree::Label) && key.value == "eager:"
|
|
249
|
+
|
|
250
|
+
bool_value(assoc.instance_variable_get(:@value))
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def bool_value(node)
|
|
254
|
+
return unless node.instance_of?(::SyntaxTree::VarRef)
|
|
255
|
+
|
|
256
|
+
value = node.instance_variable_get(:@value)
|
|
257
|
+
return true if value.respond_to?(:value) && value.value == "true"
|
|
258
|
+
return false if value.respond_to?(:value) && value.value == "false"
|
|
259
|
+
|
|
260
|
+
nil
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def unwrap_paren(node)
|
|
264
|
+
return node unless node.instance_of?(::SyntaxTree::Paren)
|
|
265
|
+
|
|
266
|
+
statements = node.instance_variable_get(:@contents)
|
|
267
|
+
body = statements&.instance_variable_get(:@body) || []
|
|
268
|
+
(body.length == 1) ? body.first : node
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def expand_import_calls(calls)
|
|
272
|
+
dependency_index = @dependency_id_offset
|
|
273
|
+
|
|
274
|
+
calls.map do |call|
|
|
275
|
+
raise_dynamic_import!(call) if call.dynamic
|
|
276
|
+
|
|
277
|
+
if call.method_name == "import_glob"
|
|
278
|
+
dependencies, watched_patterns = expand_glob_import(call, dependency_index)
|
|
279
|
+
dependency_index += dependencies.length
|
|
280
|
+
ExpandedImport.new(call, dependencies, glob_replacement_source(dependencies, eager: call.eager), watched_patterns)
|
|
281
|
+
else
|
|
282
|
+
dependency = build_dependency(call, dependency_index)
|
|
283
|
+
dependency_index += 1
|
|
284
|
+
ExpandedImport.new(call, [dependency], "#{call.replacement}(#{dependency.id.inspect})", [])
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def raise_dynamic_import!(call)
|
|
290
|
+
expected = (call.method_name == "import_glob") ? "import_glob(\"...\")" : "import(\"...\")"
|
|
291
|
+
raise DynamicImportError, "Only literal #{expected} calls are supported in #{@module_id}"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def build_dependency(call, dependency_index)
|
|
295
|
+
Dependency
|
|
296
|
+
.create(
|
|
297
|
+
specifier: call.specifier,
|
|
298
|
+
importer_id: @module_id,
|
|
299
|
+
kind: @kind,
|
|
300
|
+
loc: source_location(call.location)
|
|
301
|
+
)
|
|
302
|
+
.with(eager: call.eager)
|
|
303
|
+
.with(id: "#{@module_id}:dependency:#{dependency_index}")
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def expand_glob_import(call, dependency_index)
|
|
307
|
+
source_dir = @source_dir || raise(DynamicImportError, "Cannot expand import_glob in #{@module_id} without a source directory")
|
|
308
|
+
path_pattern, query = call.specifier.split("?", 2)
|
|
309
|
+
absolute_pattern = absolute_pattern_for(path_pattern)
|
|
310
|
+
assert_pattern_inside_source_dir!(absolute_pattern)
|
|
311
|
+
|
|
312
|
+
matches =
|
|
313
|
+
Dir
|
|
314
|
+
.glob(absolute_pattern.to_s)
|
|
315
|
+
.select { |path| File.file?(path) }
|
|
316
|
+
.sort
|
|
317
|
+
|
|
318
|
+
dependencies =
|
|
319
|
+
matches.each_with_index.map do |absolute_path, index|
|
|
320
|
+
specifier = specifier_for_glob_match(path_pattern, absolute_path)
|
|
321
|
+
specifier = "#{specifier}?#{query}" if query
|
|
322
|
+
|
|
323
|
+
Dependency
|
|
324
|
+
.create(
|
|
325
|
+
specifier: specifier,
|
|
326
|
+
importer_id: @module_id,
|
|
327
|
+
kind: @kind,
|
|
328
|
+
loc: source_location(call.location)
|
|
329
|
+
)
|
|
330
|
+
.with(eager: call.eager)
|
|
331
|
+
.with(metadata: {glob_key: specifier.split("?", 2).first})
|
|
332
|
+
.with(id: "#{@module_id}:dependency:#{dependency_index + index}")
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
watched_patterns = [
|
|
336
|
+
WatchedPattern.new(
|
|
337
|
+
@module_id,
|
|
338
|
+
absolute_pattern.relative_path_from(source_dir).to_s,
|
|
339
|
+
:import_glob,
|
|
340
|
+
{specifier: call.specifier}
|
|
341
|
+
)
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
[dependencies, watched_patterns]
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def source_location(location)
|
|
348
|
+
SourceLocation.new(
|
|
349
|
+
@module_id.to_s,
|
|
350
|
+
location.start_line + @source_line_offset,
|
|
351
|
+
location.start_column + 1 + @source_column_offset
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def source_location_for_offset(source, offset)
|
|
356
|
+
prefix = source[0...offset]
|
|
357
|
+
line = prefix.count("\n") + 1
|
|
358
|
+
column = prefix.length - (prefix.rindex("\n") || -1)
|
|
359
|
+
SourceLocation.new(@module_id.to_s, line + @source_line_offset, column + @source_column_offset)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def absolute_pattern_for(path_pattern)
|
|
363
|
+
source_dir = @source_dir
|
|
364
|
+
|
|
365
|
+
if path_pattern.start_with?("/")
|
|
366
|
+
source_dir.join(path_pattern.delete_prefix("/"))
|
|
367
|
+
elsif path_pattern.start_with?(".")
|
|
368
|
+
source_dir.join(@module_id.dirname, path_pattern)
|
|
369
|
+
else
|
|
370
|
+
source_dir.join(path_pattern)
|
|
371
|
+
end.expand_path
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def assert_pattern_inside_source_dir!(absolute_pattern)
|
|
375
|
+
pattern = absolute_pattern.to_s
|
|
376
|
+
source = @source_dir.to_s
|
|
377
|
+
return if pattern == source || pattern.start_with?("#{source}/")
|
|
378
|
+
|
|
379
|
+
raise DynamicImportError, "Glob import escapes source_dir: #{pattern}"
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def specifier_for_glob_match(path_pattern, absolute_path)
|
|
383
|
+
relative_source_path = Pathname.new(absolute_path).relative_path_from(@source_dir).to_s
|
|
384
|
+
return "/#{relative_source_path}" if path_pattern.start_with?("/")
|
|
385
|
+
return relative_source_path unless path_pattern.start_with?(".")
|
|
386
|
+
|
|
387
|
+
importer_dir = @source_dir.join(@module_id.dirname)
|
|
388
|
+
relative = Pathname.new(absolute_path).relative_path_from(importer_dir).to_s
|
|
389
|
+
relative.start_with?(".") ? relative : "./#{relative}"
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def glob_replacement_source(dependencies, eager:)
|
|
393
|
+
import_method = eager ? "__klenod_import__" : "__klenod_lazy_import__"
|
|
394
|
+
entries =
|
|
395
|
+
dependencies.map do |dependency|
|
|
396
|
+
key = dependency.metadata.fetch(:glob_key)
|
|
397
|
+
"#{key.inspect} => #{import_method}(#{dependency.id.inspect})"
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
"{#{entries.join(", ")}}"
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def rewrite_import_calls(code, expanded)
|
|
404
|
+
expanded
|
|
405
|
+
.reverse_each
|
|
406
|
+
.each_with_object(code.dup) do |import, rewritten|
|
|
407
|
+
call = import.call
|
|
408
|
+
next rewritten if call.dynamic
|
|
409
|
+
|
|
410
|
+
location = call.location
|
|
411
|
+
original = code[location.start_char...location.end_char]
|
|
412
|
+
unless original.match?(/\A#{Regexp.escape(call.method_name)}\s*\(/)
|
|
413
|
+
raise DynamicImportError, "Could not safely rewrite import at #{location.start_line}:#{location.start_column}"
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
rewritten[location.start_char...location.end_char] = import.replacement_source
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def walk(value, &block)
|
|
421
|
+
yield value
|
|
422
|
+
|
|
423
|
+
case value
|
|
424
|
+
when Array
|
|
425
|
+
value.each { |item| walk(item, &block) }
|
|
426
|
+
else
|
|
427
|
+
return unless value.respond_to?(:instance_variables)
|
|
428
|
+
|
|
429
|
+
value.instance_variables.each do |ivar|
|
|
430
|
+
walk(value.instance_variable_get(ivar), &block)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "map"
|
|
4
|
+
|
|
5
|
+
module Klenod
|
|
6
|
+
module Build
|
|
7
|
+
module SourceMap
|
|
8
|
+
Edit = Data.define(:start_offset, :end_offset, :replacement) do
|
|
9
|
+
def self.replace(start_offset, end_offset, replacement)
|
|
10
|
+
new(start_offset, end_offset, replacement)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.delete(start_offset, end_offset)
|
|
14
|
+
new(start_offset, end_offset, "")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
EditResult = Data.define(:code, :source_map)
|
|
19
|
+
|
|
20
|
+
class Editor
|
|
21
|
+
def initialize(code, source_map)
|
|
22
|
+
@code = code
|
|
23
|
+
@source_map = source_map
|
|
24
|
+
@line_starts = line_starts(code)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def apply(edits)
|
|
28
|
+
edits = edits.sort_by(&:start_offset)
|
|
29
|
+
validate_edits!(edits)
|
|
30
|
+
code = apply_code_edits(edits)
|
|
31
|
+
final_line_starts = line_starts(code)
|
|
32
|
+
|
|
33
|
+
segments =
|
|
34
|
+
@source_map.segments.filter_map do |segment|
|
|
35
|
+
offset = offset_for(segment.generated_line, segment.generated_column)
|
|
36
|
+
translated_offset = translate_offset(offset, edits)
|
|
37
|
+
next unless translated_offset
|
|
38
|
+
|
|
39
|
+
line, column = line_column_for(translated_offset, final_line_starts)
|
|
40
|
+
segment.with_generated(line, column)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
EditResult.new(code, @source_map.with(segments: segments))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def validate_edits!(edits)
|
|
49
|
+
edits.each_cons(2) do |current, successor|
|
|
50
|
+
next if current.end_offset <= successor.start_offset
|
|
51
|
+
|
|
52
|
+
raise ArgumentError, "Source map edits must not overlap"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def apply_code_edits(edits)
|
|
57
|
+
result = +""
|
|
58
|
+
offset = 0
|
|
59
|
+
|
|
60
|
+
edits.each do |edit|
|
|
61
|
+
result << @code[offset...edit.start_offset]
|
|
62
|
+
result << edit.replacement
|
|
63
|
+
offset = edit.end_offset
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
result << @code[offset..]
|
|
67
|
+
result
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def translate_offset(offset, edits)
|
|
71
|
+
delta = 0
|
|
72
|
+
|
|
73
|
+
edits.each do |edit|
|
|
74
|
+
return offset + delta if offset < edit.start_offset
|
|
75
|
+
|
|
76
|
+
if offset < edit.end_offset
|
|
77
|
+
return nil if edit.replacement.empty?
|
|
78
|
+
return edit.start_offset + delta if offset == edit.start_offset
|
|
79
|
+
|
|
80
|
+
return nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
delta += edit.replacement.length - (edit.end_offset - edit.start_offset)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
offset + delta
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def offset_for(line, column)
|
|
90
|
+
@line_starts.fetch(line) + column
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def line_starts(code)
|
|
94
|
+
starts = [0]
|
|
95
|
+
code.each_char.with_index do |char, index|
|
|
96
|
+
starts << index + 1 if char == "\n"
|
|
97
|
+
end
|
|
98
|
+
starts
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def line_column_for(offset, line_starts)
|
|
102
|
+
index = line_starts.bsearch_index { |line_start| line_start > offset }
|
|
103
|
+
line = index ? index - 1 : line_starts.length - 1
|
|
104
|
+
|
|
105
|
+
[line, offset - line_starts.fetch(line)]
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "vlq"
|
|
6
|
+
|
|
7
|
+
module Klenod
|
|
8
|
+
module Build
|
|
9
|
+
module SourceMap
|
|
10
|
+
Segment = Data.define(:generated_line, :generated_column, :source_index, :original_line, :original_column, :name_index) do
|
|
11
|
+
def with_generated(line, column)
|
|
12
|
+
self.class.new(line, column, source_index, original_line, original_column, name_index)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def with_original(line:, column:)
|
|
16
|
+
self.class.new(generated_line, generated_column, source_index, line, column, name_index)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def mapped?
|
|
20
|
+
source_index && original_line && original_column
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Map
|
|
25
|
+
attr_reader :version, :source_root, :sources, :sources_content, :names, :segments
|
|
26
|
+
|
|
27
|
+
def self.parse(json)
|
|
28
|
+
hash = JSON.parse(json)
|
|
29
|
+
new(
|
|
30
|
+
version: hash.fetch("version"),
|
|
31
|
+
source_root: hash["sourceRoot"],
|
|
32
|
+
sources: hash.fetch("sources"),
|
|
33
|
+
sources_content: hash["sourcesContent"],
|
|
34
|
+
names: hash.fetch("names", []),
|
|
35
|
+
segments: decode_mappings(hash.fetch("mappings"))
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.decode_mappings(mappings)
|
|
40
|
+
segments = []
|
|
41
|
+
previous_source_index = 0
|
|
42
|
+
previous_original_line = 0
|
|
43
|
+
previous_original_column = 0
|
|
44
|
+
previous_name_index = 0
|
|
45
|
+
|
|
46
|
+
mappings.split(";", -1).each_with_index do |line, generated_line|
|
|
47
|
+
previous_generated_column = 0
|
|
48
|
+
next if line.empty?
|
|
49
|
+
|
|
50
|
+
line.split(",").each do |encoded_segment|
|
|
51
|
+
values = VLQ.decode(encoded_segment)
|
|
52
|
+
previous_generated_column += values.fetch(0)
|
|
53
|
+
|
|
54
|
+
if values.length == 1
|
|
55
|
+
segments << Segment.new(generated_line, previous_generated_column, nil, nil, nil, nil)
|
|
56
|
+
next
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
previous_source_index += values.fetch(1)
|
|
60
|
+
previous_original_line += values.fetch(2)
|
|
61
|
+
previous_original_column += values.fetch(3)
|
|
62
|
+
name_index =
|
|
63
|
+
if values.length > 4
|
|
64
|
+
previous_name_index += values.fetch(4)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
segments << Segment.new(
|
|
68
|
+
generated_line,
|
|
69
|
+
previous_generated_column,
|
|
70
|
+
previous_source_index,
|
|
71
|
+
previous_original_line,
|
|
72
|
+
previous_original_column,
|
|
73
|
+
name_index
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
segments
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def initialize(version:, source_root:, sources:, sources_content:, names:, segments:)
|
|
82
|
+
@version = version
|
|
83
|
+
@source_root = source_root
|
|
84
|
+
@sources = sources
|
|
85
|
+
@sources_content = sources_content
|
|
86
|
+
@names = names
|
|
87
|
+
@segments = segments
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def to_json(*)
|
|
91
|
+
JSON.generate(to_h)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def to_h
|
|
95
|
+
{
|
|
96
|
+
"version" => version,
|
|
97
|
+
"sourceRoot" => source_root,
|
|
98
|
+
"mappings" => encode_mappings,
|
|
99
|
+
"sources" => sources,
|
|
100
|
+
"sourcesContent" => sources_content,
|
|
101
|
+
"names" => names
|
|
102
|
+
}.compact
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def with(sources: @sources, sources_content: @sources_content, segments: @segments)
|
|
106
|
+
self.class.new(
|
|
107
|
+
version: version,
|
|
108
|
+
source_root: source_root,
|
|
109
|
+
sources: sources,
|
|
110
|
+
sources_content: sources_content,
|
|
111
|
+
names: names,
|
|
112
|
+
segments: segments
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def map_original_lines(source_index:)
|
|
117
|
+
with(
|
|
118
|
+
segments: segments.map do |segment|
|
|
119
|
+
if segment.source_index == source_index
|
|
120
|
+
segment.with_original(line: yield(segment.original_line), column: segment.original_column)
|
|
121
|
+
else
|
|
122
|
+
segment
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def encode_mappings
|
|
129
|
+
return "" if segments.empty?
|
|
130
|
+
|
|
131
|
+
grouped = segments.group_by(&:generated_line)
|
|
132
|
+
max_line = segments.map(&:generated_line).max
|
|
133
|
+
previous_source_index = 0
|
|
134
|
+
previous_original_line = 0
|
|
135
|
+
previous_original_column = 0
|
|
136
|
+
previous_name_index = 0
|
|
137
|
+
|
|
138
|
+
(0..max_line).map do |generated_line|
|
|
139
|
+
previous_generated_column = 0
|
|
140
|
+
(grouped[generated_line] || [])
|
|
141
|
+
.sort_by(&:generated_column)
|
|
142
|
+
.map do |segment|
|
|
143
|
+
values = [segment.generated_column - previous_generated_column]
|
|
144
|
+
previous_generated_column = segment.generated_column
|
|
145
|
+
|
|
146
|
+
if segment.mapped?
|
|
147
|
+
values << segment.source_index - previous_source_index
|
|
148
|
+
values << segment.original_line - previous_original_line
|
|
149
|
+
values << segment.original_column - previous_original_column
|
|
150
|
+
previous_source_index = segment.source_index
|
|
151
|
+
previous_original_line = segment.original_line
|
|
152
|
+
previous_original_column = segment.original_column
|
|
153
|
+
|
|
154
|
+
if segment.name_index
|
|
155
|
+
values << segment.name_index - previous_name_index
|
|
156
|
+
previous_name_index = segment.name_index
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
VLQ.encode(values)
|
|
161
|
+
end
|
|
162
|
+
.join(",")
|
|
163
|
+
end.join(";")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|