klenod-build 0.0.13 → 0.0.15
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 +4 -4
- data/lib/klenod/build/context.rb +8 -2
- data/lib/klenod/build/filesystem_resolver.rb +21 -6
- data/lib/klenod/build/graph/invalidator.rb +24 -10
- data/lib/klenod/build/graph.rb +168 -27
- data/lib/klenod/build/hashing.rb +7 -0
- data/lib/klenod/build/invalidation_result.rb +17 -0
- data/lib/klenod/build/plugins/data_plugin.rb +95 -1
- data/lib/klenod/build/plugins/gem_import_plugin.rb +2 -2
- data/lib/klenod/build/plugins/google_fonts_plugin.rb +19 -1
- data/lib/klenod/build/plugins/haml_plugin/errors.rb +19 -56
- data/lib/klenod/build/plugins/haml_plugin/transformer.rb +2 -0
- data/lib/klenod/build/plugins/haml_plugin.rb +4 -0
- data/lib/klenod/build/plugins/image_plugin.rb +113 -17
- data/lib/klenod/build/plugins/intl_plugin.rb +57 -1
- data/lib/klenod/build/plugins/markdown_plugin.rb +42 -4
- data/lib/klenod/build/plugins/ruby_plugin.rb +65 -8
- data/lib/klenod/build/plugins/static_asset_plugin.rb +8 -0
- data/lib/klenod/build/plugins/svg_plugin.rb +27 -4
- data/lib/klenod/build/resolver.rb +1 -0
- data/lib/klenod/build/ruby_import_rewriter.rb +18 -1
- data/lib/klenod/build/source_error.rb +153 -0
- data/lib/klenod/build/source_excerpt.rb +84 -0
- data/lib/klenod/build/version.rb +1 -1
- data/lib/klenod/build/watcher.rb +10 -1
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: abf589452fadde26fe16d0bbc326e0161e8ed8a719ea516e12e286ef8e1609aa
|
|
4
|
+
data.tar.gz: 9f54e971b5dc88cd07348be0d96e9885928db10c64b4228bfb64e350b202f669
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: af84591cca45a914ffbf087f308eea54d264cb14a0e03d6e60732a806d5e4cf44b00d291679b1f3dfe1f6d39f58bda043ff6b1de625b9d904667d2db5f8a6c07
|
|
7
|
+
data.tar.gz: 8f5fbdc8411f73fee7b7f6b86f803d7e04e588db97dc3d4d5019b7bf08cc63a1d73b05d82a97e74135fc2370be072b00524b7df3ac301c0ed2f4eb025c4352d7
|
data/lib/klenod/build/context.rb
CHANGED
|
@@ -113,7 +113,8 @@ module Klenod
|
|
|
113
113
|
asset_generation_concurrency: AssetGenerationQueue::DEFAULT_CONCURRENCY,
|
|
114
114
|
asset_download_concurrency: AssetGenerationQueue::DEFAULT_DOWNLOAD_CONCURRENCY,
|
|
115
115
|
profiler: nil,
|
|
116
|
-
namespace: Module.new
|
|
116
|
+
namespace: Module.new,
|
|
117
|
+
analysis: false
|
|
117
118
|
)
|
|
118
119
|
@source_dir = source_dir
|
|
119
120
|
plugins = plugins.to_a if plugins.equal?(DEFAULT_PLUGINS)
|
|
@@ -131,12 +132,17 @@ module Klenod
|
|
|
131
132
|
asset_generation_concurrency: asset_generation_concurrency,
|
|
132
133
|
asset_download_concurrency: asset_download_concurrency,
|
|
133
134
|
profiler: profiler,
|
|
134
|
-
namespace: namespace
|
|
135
|
+
namespace: namespace,
|
|
136
|
+
analysis: analysis
|
|
135
137
|
)
|
|
136
138
|
end
|
|
137
139
|
|
|
138
140
|
attr_reader :graph, :mode, :base, :asset_origin
|
|
139
141
|
|
|
142
|
+
def analysis?
|
|
143
|
+
@graph.analysis?
|
|
144
|
+
end
|
|
145
|
+
|
|
140
146
|
def evaluate(specifier)
|
|
141
147
|
@graph.load(specifier)
|
|
142
148
|
end
|
|
@@ -15,6 +15,14 @@ module Klenod
|
|
|
15
15
|
@root = Pathname.new(root).expand_path
|
|
16
16
|
@extensions = extensions.freeze
|
|
17
17
|
@path_prefix = path_prefix.to_s
|
|
18
|
+
@spelling_index = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Forget the cached file listing behind did-you-mean suggestions. The
|
|
22
|
+
# owning resolver clears it whenever files change, so a suggestion never
|
|
23
|
+
# walks the source tree more than once between file events.
|
|
24
|
+
def clear_cache
|
|
25
|
+
@spelling_index = nil
|
|
18
26
|
end
|
|
19
27
|
|
|
20
28
|
def resolve(relative_path)
|
|
@@ -101,14 +109,21 @@ module Klenod
|
|
|
101
109
|
[]
|
|
102
110
|
end
|
|
103
111
|
|
|
112
|
+
SpellingIndex = Data.define(:aliases, :checker)
|
|
113
|
+
|
|
104
114
|
def spelling_corrections(relative_path)
|
|
105
|
-
|
|
106
|
-
matches =
|
|
107
|
-
DidYouMean::TreeSpellChecker
|
|
108
|
-
.new(dictionary: aliases.keys.sort, separator: "/")
|
|
109
|
-
.correct(relative_path)
|
|
115
|
+
index = spelling_index
|
|
116
|
+
matches = index.checker.correct(relative_path)
|
|
110
117
|
|
|
111
|
-
matches.flat_map { aliases.fetch(it) }.uniq.first(MAX_CORRECTIONS)
|
|
118
|
+
matches.flat_map { index.aliases.fetch(it) }.uniq.first(MAX_CORRECTIONS)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def spelling_index
|
|
122
|
+
@spelling_index ||=
|
|
123
|
+
begin
|
|
124
|
+
aliases = spelling_aliases
|
|
125
|
+
SpellingIndex.new(aliases, DidYouMean::TreeSpellChecker.new(dictionary: aliases.keys.sort, separator: "/"))
|
|
126
|
+
end
|
|
112
127
|
end
|
|
113
128
|
|
|
114
129
|
def spelling_aliases
|
|
@@ -24,10 +24,11 @@ module Klenod
|
|
|
24
24
|
errors = []
|
|
25
25
|
|
|
26
26
|
removed_module_ids.each do |module_id|
|
|
27
|
-
|
|
27
|
+
graph.remove_record(module_id)
|
|
28
28
|
mods.delete(module_id)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
failed_reload_ids = []
|
|
31
32
|
reloaded_module_ids =
|
|
32
33
|
reload_module_ids.filter_map do |module_id|
|
|
33
34
|
if evaluated_module_ids.include?(module_id)
|
|
@@ -36,12 +37,15 @@ module Klenod
|
|
|
36
37
|
graph.collect_module(module_id, force: true)
|
|
37
38
|
end
|
|
38
39
|
module_id
|
|
39
|
-
rescue => e
|
|
40
|
+
rescue StandardError, ScriptError => e
|
|
41
|
+
# ScriptError too: a syntax error in a module is not a
|
|
42
|
+
# StandardError, and letting it escape here kills the watcher
|
|
43
|
+
# thread rather than reporting the module that failed.
|
|
40
44
|
mark_module_failed(module_id, e)
|
|
41
|
-
|
|
45
|
+
failed_reload_ids << module_id
|
|
46
|
+
record_error(errors, module_id, e)
|
|
42
47
|
nil
|
|
43
48
|
end
|
|
44
|
-
failed_reload_ids = errors.map(&:first) & reload_module_ids
|
|
45
49
|
blocked_dependent_ids = dependent_closure(failed_reload_ids)
|
|
46
50
|
blocked_dependent_ids.each { |module_id| mods.delete(module_id) }
|
|
47
51
|
|
|
@@ -58,8 +62,8 @@ module Klenod
|
|
|
58
62
|
graph.collect_module(module_id, force: true)
|
|
59
63
|
nil
|
|
60
64
|
end
|
|
61
|
-
rescue => e
|
|
62
|
-
errors
|
|
65
|
+
rescue StandardError, ScriptError => e
|
|
66
|
+
record_error(errors, module_id, e)
|
|
63
67
|
nil
|
|
64
68
|
end
|
|
65
69
|
asset_updates = diff_assets(previous_assets, graph.assets)
|
|
@@ -80,6 +84,16 @@ module Klenod
|
|
|
80
84
|
|
|
81
85
|
private
|
|
82
86
|
|
|
87
|
+
# One failure, reported once. A module whose dependency already failed
|
|
88
|
+
# in this invalidation re-raises that same error when it reloads, so a
|
|
89
|
+
# broken companion would otherwise be reported once for itself and
|
|
90
|
+
# again for every module that imports it.
|
|
91
|
+
def record_error(errors, module_id, error)
|
|
92
|
+
return if errors.any? { |(_id, recorded)| recorded.equal?(error) }
|
|
93
|
+
|
|
94
|
+
errors << [module_id, error]
|
|
95
|
+
end
|
|
96
|
+
|
|
83
97
|
attr_reader :graph, :resolver, :source_loader
|
|
84
98
|
|
|
85
99
|
def records
|
|
@@ -175,9 +189,7 @@ module Klenod
|
|
|
175
189
|
end
|
|
176
190
|
|
|
177
191
|
def direct_dependents(module_id)
|
|
178
|
-
|
|
179
|
-
candidate_id if record.resolved_dependencies.any? { |dependency| dependency.module_id == module_id }
|
|
180
|
-
end
|
|
192
|
+
graph.dependents(module_id)
|
|
181
193
|
end
|
|
182
194
|
|
|
183
195
|
def mark_module_failed(module_id, error)
|
|
@@ -192,7 +204,8 @@ module Klenod
|
|
|
192
204
|
source_hash = loaded_source.source_hash || Hashing.hexdigest(source)
|
|
193
205
|
version = cached ? cached.version + 1 : 0
|
|
194
206
|
|
|
195
|
-
|
|
207
|
+
graph.store_record(
|
|
208
|
+
module_id,
|
|
196
209
|
ModuleRecord.new(
|
|
197
210
|
module_id,
|
|
198
211
|
source_hash,
|
|
@@ -208,6 +221,7 @@ module Klenod
|
|
|
208
221
|
version,
|
|
209
222
|
:failed
|
|
210
223
|
)
|
|
224
|
+
)
|
|
211
225
|
mods[module_id] = FailedModule.new(error)
|
|
212
226
|
end
|
|
213
227
|
|
data/lib/klenod/build/graph.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "klenod/runtime/mod"
|
|
|
7
7
|
require "klenod/runtime/bundle"
|
|
8
8
|
require_relative "asset_generation_queue"
|
|
9
9
|
require_relative "errors"
|
|
10
|
+
require_relative "source_error"
|
|
10
11
|
require_relative "graph/invalidator"
|
|
11
12
|
require_relative "hashing"
|
|
12
13
|
require_relative "invalidation_result"
|
|
@@ -24,9 +25,12 @@ module Klenod
|
|
|
24
25
|
include TSort
|
|
25
26
|
|
|
26
27
|
AsyncResult = Data.define(:value, :error) do
|
|
28
|
+
# ScriptError too, so a syntax error in a module comes back to the
|
|
29
|
+
# parent graph path instead of surfacing as an unhandled Async::Task
|
|
30
|
+
# exception.
|
|
27
31
|
def self.capture
|
|
28
32
|
new(yield, nil)
|
|
29
|
-
rescue => e
|
|
33
|
+
rescue StandardError, ScriptError => e
|
|
30
34
|
new(nil, e)
|
|
31
35
|
end
|
|
32
36
|
|
|
@@ -67,9 +71,11 @@ module Klenod
|
|
|
67
71
|
asset_generation_concurrency: AssetGenerationQueue::DEFAULT_CONCURRENCY,
|
|
68
72
|
asset_download_concurrency: AssetGenerationQueue::DEFAULT_DOWNLOAD_CONCURRENCY,
|
|
69
73
|
profiler: nil,
|
|
70
|
-
namespace: Module.new
|
|
74
|
+
namespace: Module.new,
|
|
75
|
+
analysis: false
|
|
71
76
|
)
|
|
72
77
|
@profiler = profiler || Profiler.new
|
|
78
|
+
@analysis = analysis
|
|
73
79
|
@resolver = Resolver.new(source_dir: source_dir, profiler: @profiler)
|
|
74
80
|
@plugins = plugins
|
|
75
81
|
@mode = mode
|
|
@@ -86,6 +92,16 @@ module Klenod
|
|
|
86
92
|
@virtual_metadata = {}
|
|
87
93
|
@virtual_owners = {}
|
|
88
94
|
@loading_tasks = {}
|
|
95
|
+
@source_overrides = {}
|
|
96
|
+
@dependents = {}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Analysis mode collects the graph for tooling such as the language
|
|
100
|
+
# server. Plugins must keep the record shape but skip work whose result
|
|
101
|
+
# is never used there: network fetches, hashing whole files, compiling
|
|
102
|
+
# JavaScript, and building asset bytes.
|
|
103
|
+
def analysis?
|
|
104
|
+
@analysis
|
|
89
105
|
end
|
|
90
106
|
|
|
91
107
|
def load(specifier)
|
|
@@ -207,10 +223,89 @@ module Klenod
|
|
|
207
223
|
end
|
|
208
224
|
end
|
|
209
225
|
|
|
226
|
+
# Editor buffers take precedence over files on disk for the given
|
|
227
|
+
# module until cleared. The next collection re-reads and, when the text
|
|
228
|
+
# changed, re-transforms the module. A caller that already transformed
|
|
229
|
+
# the same text can pass that `transform` so collection reuses it.
|
|
230
|
+
def override_source(module_id, source, transform: nil)
|
|
231
|
+
@source_overrides[module_id] = LoadResult.new(source, nil, transform)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def clear_source_override(module_id)
|
|
235
|
+
@source_overrides.delete(module_id)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def source_override?(module_id)
|
|
239
|
+
@source_overrides.key?(module_id)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Module ids whose collected record imports the given module, eagerly
|
|
243
|
+
# or lazily.
|
|
244
|
+
def dependents(module_id)
|
|
245
|
+
@dependents.fetch(module_id) { [] }.to_a
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def store_record(module_id, record)
|
|
249
|
+
previous = @records[module_id]
|
|
250
|
+
previous&.resolved_dependencies&.each { |dependency| @dependents[dependency.module_id]&.delete(module_id) }
|
|
251
|
+
record.resolved_dependencies.each { |dependency| (@dependents[dependency.module_id] ||= Set.new) << module_id }
|
|
252
|
+
@records[module_id] = record
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def remove_record(module_id)
|
|
256
|
+
record = @records.delete(module_id)
|
|
257
|
+
record&.resolved_dependencies&.each { |dependency| @dependents[dependency.module_id]&.delete(module_id) }
|
|
258
|
+
record
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Collect everything reachable from a collected module through eager
|
|
262
|
+
# and lazy dependencies. Each module the walk collects itself is
|
|
263
|
+
# yielded with `nil`; a module that fails to collect is yielded with
|
|
264
|
+
# its error and the walk continues past it.
|
|
265
|
+
def collect_reachable(module_id)
|
|
266
|
+
queue = @records.fetch(module_id).resolved_dependencies.dup
|
|
267
|
+
seen = Set.new([module_id])
|
|
268
|
+
index = 0
|
|
269
|
+
|
|
270
|
+
while index < queue.length
|
|
271
|
+
dependency_id = queue[index].module_id
|
|
272
|
+
index += 1
|
|
273
|
+
next unless seen.add?(dependency_id)
|
|
274
|
+
|
|
275
|
+
record = @records[dependency_id]
|
|
276
|
+
unless record
|
|
277
|
+
begin
|
|
278
|
+
record = collect_module(dependency_id)
|
|
279
|
+
rescue StandardError, ScriptError => error
|
|
280
|
+
yield dependency_id, error if block_given?
|
|
281
|
+
next
|
|
282
|
+
end
|
|
283
|
+
yield dependency_id, nil if block_given?
|
|
284
|
+
end
|
|
285
|
+
queue.concat(record.resolved_dependencies)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Transform source for a module without collecting it. Editor tooling
|
|
290
|
+
# such as the language server uses this to analyze unsaved text with the
|
|
291
|
+
# same plugins as a build, without adding a record or evaluating anything.
|
|
292
|
+
def transform_source(module_id, source)
|
|
293
|
+
transform = transform_module_source(module_id, source)
|
|
294
|
+
assert_supported_transform!(module_id, source, transform)
|
|
295
|
+
transform
|
|
296
|
+
end
|
|
297
|
+
|
|
210
298
|
def absolute_path(module_id)
|
|
211
299
|
@resolver.absolute_path(module_id)
|
|
212
300
|
end
|
|
213
301
|
|
|
302
|
+
# Forget resolved paths so files created, renamed, or deleted outside
|
|
303
|
+
# a build invalidation are looked up again. Invalidation does this
|
|
304
|
+
# itself; tooling that only transforms must ask for it.
|
|
305
|
+
def clear_resolver_cache
|
|
306
|
+
@resolver.clear_cache
|
|
307
|
+
end
|
|
308
|
+
|
|
214
309
|
def source_dir
|
|
215
310
|
@resolver.source_dir
|
|
216
311
|
end
|
|
@@ -231,7 +326,7 @@ module Klenod
|
|
|
231
326
|
@virtual_sources.delete(module_id)
|
|
232
327
|
@virtual_metadata.delete(module_id)
|
|
233
328
|
@virtual_owners.delete(module_id)
|
|
234
|
-
|
|
329
|
+
remove_record(module_id)
|
|
235
330
|
@mods.delete(module_id)
|
|
236
331
|
end
|
|
237
332
|
end
|
|
@@ -281,11 +376,12 @@ module Klenod
|
|
|
281
376
|
dependency_records = load_eager_dependency_records(resolved_dependencies)
|
|
282
377
|
transform = finalize_transform_result(module_id, transform, resolved_dependencies, dependency_records)
|
|
283
378
|
assert_supported_transform!(module_id, source, transform)
|
|
379
|
+
assert_generated_ruby_parses!(module_id, transform)
|
|
284
380
|
transformed_hash = Hashing.hexdigest(transform.code)
|
|
285
|
-
mod = instantiate_module(module_id, transform, resolved_dependencies, dependency_records, cached)
|
|
381
|
+
mod = instantiate_module(module_id, transform, resolved_dependencies, dependency_records, cached, source)
|
|
286
382
|
record = build_module_record(module_id, source, source_hash, transformed_hash, transform, resolved_dependencies, mod)
|
|
287
383
|
|
|
288
|
-
|
|
384
|
+
store_record(module_id, record)
|
|
289
385
|
@mods[module_id] = mod
|
|
290
386
|
record
|
|
291
387
|
end
|
|
@@ -336,10 +432,11 @@ module Klenod
|
|
|
336
432
|
dependency_records = collect_eager_dependency_records(resolved_dependencies)
|
|
337
433
|
transform = finalize_transform_result(module_id, transform, resolved_dependencies, dependency_records)
|
|
338
434
|
assert_supported_transform!(module_id, source, transform)
|
|
435
|
+
assert_generated_ruby_parses!(module_id, transform)
|
|
339
436
|
transformed_hash = Hashing.hexdigest(transform.code)
|
|
340
437
|
record = build_module_record(module_id, source, source_hash, transformed_hash, transform, resolved_dependencies, cached)
|
|
341
438
|
|
|
342
|
-
|
|
439
|
+
store_record(module_id, record)
|
|
343
440
|
@mods.delete(module_id)
|
|
344
441
|
@profiler.progress(:collect_module, module_id: record.id.to_s, total_records: @records.length)
|
|
345
442
|
record
|
|
@@ -347,22 +444,31 @@ module Klenod
|
|
|
347
444
|
end
|
|
348
445
|
|
|
349
446
|
def evaluate_module(module_id)
|
|
350
|
-
|
|
447
|
+
if @mods.key?(module_id)
|
|
448
|
+
mod = @mods.fetch(module_id)
|
|
449
|
+
# A failed reload is remembered so later demand raises the stored
|
|
450
|
+
# error rather than serving stale exports.
|
|
451
|
+
raise mod.error if mod.is_a?(FailedModule)
|
|
452
|
+
|
|
453
|
+
return mod
|
|
454
|
+
end
|
|
351
455
|
|
|
352
456
|
record = @records.fetch(module_id) { collect_module(module_id) }
|
|
353
457
|
raise_failed_module!(record)
|
|
354
458
|
evaluate_eager_dependencies(record.resolved_dependencies)
|
|
355
459
|
dependency_records = dependency_records_for(eager_dependencies(record.resolved_dependencies))
|
|
356
460
|
mod =
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
461
|
+
evaluating(module_id, record.transformed_source, record.source) do
|
|
462
|
+
Runtime::Mod.new(
|
|
463
|
+
module_id.to_s,
|
|
464
|
+
record.transformed_source,
|
|
465
|
+
imports: imports_for(record.resolved_dependencies, dependency_records),
|
|
466
|
+
source_map: record.source_map,
|
|
467
|
+
version: record.version,
|
|
468
|
+
eval_path: eval_path_for(module_id),
|
|
469
|
+
namespace: namespace
|
|
470
|
+
)
|
|
471
|
+
end
|
|
366
472
|
|
|
367
473
|
@mods[module_id] = mod
|
|
368
474
|
end
|
|
@@ -532,20 +638,50 @@ module Klenod
|
|
|
532
638
|
raise UnsupportedFileError, "No plugin transformed #{module_id.path.inspect}. Add a plugin for #{module_id.extname.inspect} files."
|
|
533
639
|
end
|
|
534
640
|
|
|
641
|
+
# A plugin that generates Ruby can generate Ruby that does not parse.
|
|
642
|
+
# Without this the failure waits until the module is evaluated, and a
|
|
643
|
+
# production build never evaluates application modules, so the bundle
|
|
644
|
+
# would ship broken. Ruby modules are already checked by RubyPlugin
|
|
645
|
+
# against their original source, which reports better locations.
|
|
646
|
+
def assert_generated_ruby_parses!(module_id, transform)
|
|
647
|
+
return if ruby_module_extension?(module_id.extname)
|
|
648
|
+
return if Prism.parse_success?(transform.code)
|
|
649
|
+
|
|
650
|
+
raise GeneratedRubyError.new(Prism.parse(transform.code), source: transform.code, module_id: module_id)
|
|
651
|
+
end
|
|
652
|
+
|
|
535
653
|
def ruby_module_extension?(extname)
|
|
536
654
|
extname.empty? || extname == ".rb"
|
|
537
655
|
end
|
|
538
656
|
|
|
539
|
-
def instantiate_module(module_id, transform, resolved_dependencies, dependency_records, cached)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
657
|
+
def instantiate_module(module_id, transform, resolved_dependencies, dependency_records, cached, source)
|
|
658
|
+
evaluating(module_id, transform.code, source) do
|
|
659
|
+
Runtime::Mod.new(
|
|
660
|
+
module_id.to_s,
|
|
661
|
+
transform.code,
|
|
662
|
+
imports: imports_for(resolved_dependencies, dependency_records),
|
|
663
|
+
source_map: transform.source_map,
|
|
664
|
+
version: cached ? cached.version + 1 : 0,
|
|
665
|
+
eval_path: eval_path_for(module_id),
|
|
666
|
+
namespace: namespace
|
|
667
|
+
)
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# Evaluating a module runs its Ruby, and a syntax error there is a
|
|
672
|
+
# ScriptError that escapes every `rescue => e` in the build. Only a syntax
|
|
673
|
+
# error is wrapped: a LoadError from the module's own `require` is also a
|
|
674
|
+
# ScriptError, and reporting that as a syntax error would be wrong.
|
|
675
|
+
#
|
|
676
|
+
# The reported line refers to the evaluated source, so the excerpt has to
|
|
677
|
+
# come from that -- except for a Ruby module, whose transform only
|
|
678
|
+
# rewrites import calls in place and leaves every line where it was. There
|
|
679
|
+
# the original source reads better.
|
|
680
|
+
def evaluating(module_id, evaluated_source, original_source)
|
|
681
|
+
yield
|
|
682
|
+
rescue ::SyntaxError => error
|
|
683
|
+
excerpt_source = ruby_module_extension?(module_id.extname) ? original_source : evaluated_source
|
|
684
|
+
raise GeneratedRubyError.new(error, source: excerpt_source || evaluated_source, module_id: module_id)
|
|
549
685
|
end
|
|
550
686
|
|
|
551
687
|
def eval_path_for(module_id)
|
|
@@ -670,7 +806,7 @@ module Klenod
|
|
|
670
806
|
|
|
671
807
|
tasks.each_with_object({}) do |(dependency_id, child_task), records|
|
|
672
808
|
records[dependency_id] = AsyncResult.unwrap(child_task.wait)
|
|
673
|
-
rescue => e
|
|
809
|
+
rescue StandardError, ScriptError => e
|
|
674
810
|
first_error ||= e
|
|
675
811
|
end.tap do
|
|
676
812
|
raise first_error if first_error
|
|
@@ -806,6 +942,11 @@ module Klenod
|
|
|
806
942
|
end
|
|
807
943
|
|
|
808
944
|
def load_source(module_id)
|
|
945
|
+
if (override = @source_overrides[module_id])
|
|
946
|
+
@profiler.count(:source_override_hit)
|
|
947
|
+
return override
|
|
948
|
+
end
|
|
949
|
+
|
|
809
950
|
@plugins.each do |plugin|
|
|
810
951
|
@profiler.count(:plugin_load_check)
|
|
811
952
|
loaded =
|
data/lib/klenod/build/hashing.rb
CHANGED
|
@@ -13,6 +13,13 @@ module Klenod
|
|
|
13
13
|
Digest::SHA256.file(path.to_s).hexdigest
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# A cheap change-detection key for a file when its bytes are not
|
|
17
|
+
# needed, e.g. graph analysis that skips asset work.
|
|
18
|
+
def self.file_key(path)
|
|
19
|
+
stat = File.stat(path.to_s)
|
|
20
|
+
short("#{path}:#{stat.mtime.to_f}:#{stat.size}")
|
|
21
|
+
end
|
|
22
|
+
|
|
16
23
|
def self.short(value, length: 16)
|
|
17
24
|
hexdigest(value)[0, length]
|
|
18
25
|
end
|
|
@@ -38,6 +38,23 @@ module Klenod
|
|
|
38
38
|
:asset_updates,
|
|
39
39
|
:errors
|
|
40
40
|
) do
|
|
41
|
+
# An invalidation that blew up before it could inspect any module, so
|
|
42
|
+
# subscribers still get told about the failure instead of the build
|
|
43
|
+
# going quiet.
|
|
44
|
+
def self.failed(error, module_id: nil)
|
|
45
|
+
new(
|
|
46
|
+
changed_module_ids: [].freeze,
|
|
47
|
+
removed_module_ids: [].freeze,
|
|
48
|
+
reloaded_module_ids: [].freeze,
|
|
49
|
+
reevaluated_module_ids: [].freeze,
|
|
50
|
+
added_assets: [].freeze,
|
|
51
|
+
changed_assets: [].freeze,
|
|
52
|
+
removed_assets: [].freeze,
|
|
53
|
+
asset_updates: [].freeze,
|
|
54
|
+
errors: [[module_id, error]].freeze
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
41
58
|
def asset_changes
|
|
42
59
|
AssetChanges.new(added_assets, changed_assets, removed_assets)
|
|
43
60
|
end
|
|
@@ -5,6 +5,7 @@ require "toml-rb"
|
|
|
5
5
|
require "yaml"
|
|
6
6
|
|
|
7
7
|
require_relative "../plugin"
|
|
8
|
+
require_relative "../source_error"
|
|
8
9
|
require_relative "../transform_result"
|
|
9
10
|
|
|
10
11
|
module Klenod
|
|
@@ -16,14 +17,24 @@ module Klenod
|
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
class Plugin < Klenod::Build::Plugin
|
|
20
|
+
# A format that cannot fail to parse declares no wrapped errors, and
|
|
21
|
+
# `rescue *[]` then catches nothing.
|
|
22
|
+
WRAPPED_ERRORS = [].freeze
|
|
23
|
+
|
|
19
24
|
def self.extensions(*values)
|
|
20
25
|
const_set(:EXTENSIONS, values.freeze)
|
|
21
26
|
end
|
|
22
27
|
|
|
28
|
+
# Names the SourceError to raise and the library exceptions it wraps.
|
|
29
|
+
def self.parse_error(error_class, *wrapped)
|
|
30
|
+
const_set(:PARSE_ERROR, error_class)
|
|
31
|
+
const_set(:WRAPPED_ERRORS, wrapped.freeze)
|
|
32
|
+
end
|
|
33
|
+
|
|
23
34
|
def transform(module_id, code, _context)
|
|
24
35
|
return super unless self.class::EXTENSIONS.include?(module_id.extname)
|
|
25
36
|
|
|
26
|
-
data =
|
|
37
|
+
data = parse_source(code, module_id)
|
|
27
38
|
TransformResult.new(module_source(data), [], nil, [], [], {data: data})
|
|
28
39
|
end
|
|
29
40
|
|
|
@@ -41,6 +52,15 @@ module Klenod
|
|
|
41
52
|
|
|
42
53
|
private
|
|
43
54
|
|
|
55
|
+
# Each format's library raises its own exception type. Wrapping them
|
|
56
|
+
# here keeps every `parse` hook a one-liner and gives all four formats
|
|
57
|
+
# the same report.
|
|
58
|
+
def parse_source(code, module_id)
|
|
59
|
+
parse(code)
|
|
60
|
+
rescue *self.class::WRAPPED_ERRORS => error
|
|
61
|
+
raise self.class::PARSE_ERROR.new(error, source: code, module_id: module_id)
|
|
62
|
+
end
|
|
63
|
+
|
|
44
64
|
def module_source(data)
|
|
45
65
|
<<~RUBY
|
|
46
66
|
Default = #{data.inspect}
|
|
@@ -54,8 +74,32 @@ module Klenod
|
|
|
54
74
|
Plugin.new(...)
|
|
55
75
|
end
|
|
56
76
|
|
|
77
|
+
class ParseError < Klenod::Build::SourceError
|
|
78
|
+
# "expected ':' after object key, got: '2' at line 3 column 7"
|
|
79
|
+
LOCATION = /\s+at line \d+ column \d+\z/
|
|
80
|
+
|
|
81
|
+
def kind
|
|
82
|
+
"JSON parse error"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def location(error)
|
|
88
|
+
return nil unless error.line
|
|
89
|
+
|
|
90
|
+
# The location is rendered as the title and the caret, so drop the
|
|
91
|
+
# copy the parser appends to its message.
|
|
92
|
+
Location.new(
|
|
93
|
+
line: error.line,
|
|
94
|
+
column: error.column,
|
|
95
|
+
detail: error.message.sub(LOCATION, "")
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
57
100
|
class Plugin < DataPlugin::Plugin
|
|
58
101
|
extensions ".json"
|
|
102
|
+
parse_error ParseError, JSON::ParserError
|
|
59
103
|
|
|
60
104
|
private
|
|
61
105
|
|
|
@@ -70,8 +114,32 @@ module Klenod
|
|
|
70
114
|
Plugin.new(...)
|
|
71
115
|
end
|
|
72
116
|
|
|
117
|
+
class ParseError < Klenod::Build::SourceError
|
|
118
|
+
def kind
|
|
119
|
+
"YAML parse error"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def location(error)
|
|
125
|
+
# Psych::DisallowedClass and friends carry no location.
|
|
126
|
+
return nil unless error.is_a?(Psych::SyntaxError)
|
|
127
|
+
|
|
128
|
+
# One-based. libyaml points at the construct it was parsing when it
|
|
129
|
+
# gave up, which is not always the offending line, so `context` is
|
|
130
|
+
# worth surfacing.
|
|
131
|
+
Location.new(
|
|
132
|
+
line: error.line,
|
|
133
|
+
column: error.column,
|
|
134
|
+
detail: error.problem,
|
|
135
|
+
hints: [error.context&.capitalize].compact
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
73
140
|
class Plugin < DataPlugin::Plugin
|
|
74
141
|
extensions ".yaml", ".yml"
|
|
142
|
+
parse_error ParseError, Psych::Exception
|
|
75
143
|
|
|
76
144
|
private
|
|
77
145
|
|
|
@@ -86,8 +154,34 @@ module Klenod
|
|
|
86
154
|
Plugin.new(...)
|
|
87
155
|
end
|
|
88
156
|
|
|
157
|
+
class ParseError < Klenod::Build::SourceError
|
|
158
|
+
# "Failed to parse input on line 2 at offset 9\ninvalid =\n\n ^"
|
|
159
|
+
LOCATION = /\AFailed to parse input on line (?<line>\d+) at offset (?<column>\d+)/
|
|
160
|
+
|
|
161
|
+
def kind
|
|
162
|
+
"TOML parse error"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
def location(error)
|
|
168
|
+
found = LOCATION.match(error.message)
|
|
169
|
+
return nil unless found
|
|
170
|
+
|
|
171
|
+
# toml-rb embeds its own caret diagram in the message; we render our
|
|
172
|
+
# own from the line and column instead.
|
|
173
|
+
Location.new(
|
|
174
|
+
line: found[:line].to_i,
|
|
175
|
+
# citrus reports a zero-based offset into the line.
|
|
176
|
+
column: found[:column].to_i + 1,
|
|
177
|
+
detail: "Could not parse TOML"
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
89
182
|
class Plugin < DataPlugin::Plugin
|
|
90
183
|
extensions ".toml"
|
|
184
|
+
parse_error ParseError, TomlRB::Error
|
|
91
185
|
|
|
92
186
|
private
|
|
93
187
|
|
|
@@ -39,11 +39,11 @@ module Klenod
|
|
|
39
39
|
raise error.with_resolution_context(dependency: dependency, importer_id: dependency.importer_id)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def load(module_id,
|
|
42
|
+
def load(module_id, context)
|
|
43
43
|
return nil unless module_id.scheme == :gem
|
|
44
44
|
|
|
45
45
|
path = path_for(module_id)
|
|
46
|
-
LoadResult.new(path.binread, Hashing.file_hexdigest(path), nil)
|
|
46
|
+
LoadResult.new(path.binread, context.analysis? ? Hashing.file_key(path) : Hashing.file_hexdigest(path), nil)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
private
|