klenod-build 0.0.24 → 0.0.26
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/exception_formatter.rb +81 -0
- data/lib/klenod/build/graph/invalidator.rb +22 -21
- data/lib/klenod/build/graph.rb +15 -0
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.json +37 -1
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.txt +1 -1
- data/lib/klenod/build/plugins/haml_plugin/parser.rb +26 -1
- data/lib/klenod/build/resolution_error_formatter.rb +3 -14
- data/lib/klenod/build/source_error.rb +21 -9
- data/lib/klenod/build/source_excerpt.rb +135 -31
- data/lib/klenod/build/version.rb +1 -1
- data/lib/klenod/build.rb +2 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2b1c963370596392fc4e48311db021c836c182e27665d8b62652bda1a55744e2
|
|
4
|
+
data.tar.gz: 454ee9440b137797885ee3b45d5fecc789fb64cc7adcf43e9a61eead08602299
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fc351535e38a0599165dc4794cecfdc84064b5cf8b0273f7e3f763643a152d43391f413a2ab2d97845f0d3774c41989c925da95c4f2d12e32261569e963ec34c
|
|
7
|
+
data.tar.gz: 0e8b90fbaef71ccb01e419ac58e4f394cafe69c61d2926bac649802404a075cf89c1ee8df77b9cf9cc4acf6e958b3547bd9e338cc4ef6ba85e4d158d0ef2993d
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "klenod/runtime/backtrace_rewriter"
|
|
4
|
+
|
|
5
|
+
require_relative "source_excerpt"
|
|
6
|
+
|
|
7
|
+
module Klenod
|
|
8
|
+
module Build
|
|
9
|
+
# A runtime exception as a development report, in the same shape as a
|
|
10
|
+
# SourceError: the class and message, an excerpt of the first frame that
|
|
11
|
+
# has source, then the frames.
|
|
12
|
+
#
|
|
13
|
+
# × NameError: undefined local variable or method 'x'
|
|
14
|
+
#
|
|
15
|
+
# ╭─[app:/routes/page.haml:4]
|
|
16
|
+
# 3 │ %h1 Hello
|
|
17
|
+
# > 4 │ %p= x
|
|
18
|
+
# ╰────
|
|
19
|
+
# at app:/routes/page.haml:4:in 'render'
|
|
20
|
+
# at /gems/rack/lib/rack.rb:12:in 'call'
|
|
21
|
+
#
|
|
22
|
+
# Rewriting lives in Klenod::Runtime::BacktraceRewriter, so a production
|
|
23
|
+
# application keeps corrected backtraces without loading any of this.
|
|
24
|
+
module ExceptionFormatter
|
|
25
|
+
Entry = Runtime::BacktraceRewriter::ParsedBacktraceEntry
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# `mods` maps module IDs to evaluated modules, as Graph#mods does. The
|
|
30
|
+
# error is rewritten in place.
|
|
31
|
+
def format(error, mods: {}, ansi: true, links: ansi && SourceExcerpt.hyperlinks?)
|
|
32
|
+
rewriter = Runtime::BacktraceRewriter.new(mods)
|
|
33
|
+
rewriter.rewrite_exception(error)
|
|
34
|
+
module_ids = mods.values.select { it.respond_to?(:eval_path) }.to_h { [it.eval_path.to_s, it.path.to_s] }
|
|
35
|
+
frames = Array(error.backtrace).map { Entry.parse(it) || it.to_s }
|
|
36
|
+
|
|
37
|
+
[
|
|
38
|
+
SourceExcerpt.header(error.class.name, error.message, ansi:),
|
|
39
|
+
[
|
|
40
|
+
excerpt(frames, rewriter, module_ids, ansi:, links:),
|
|
41
|
+
*frames.map { " #{frame(it, module_ids, ansi:, links:)}" }
|
|
42
|
+
].compact.join("\n")
|
|
43
|
+
].reject(&:empty?).join("\n\n")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The first frame the rewriter has original source for, boxed.
|
|
47
|
+
def excerpt(frames, rewriter, module_ids, ansi:, links:)
|
|
48
|
+
frames.grep(Entry).each do |entry|
|
|
49
|
+
source = rewriter.source_for(entry.file)
|
|
50
|
+
next unless source
|
|
51
|
+
|
|
52
|
+
location = location(entry, module_ids, ansi:, links:)
|
|
53
|
+
excerpt = SourceExcerpt.excerpt(source:, line: entry.line, ansi:, location:)
|
|
54
|
+
return excerpt if excerpt
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# "at app:/routes/page.haml:4:in 'render'". Frames outside the graph are
|
|
61
|
+
# dimmed so the application's own frames stand out.
|
|
62
|
+
def frame(entry, module_ids, ansi:, links:)
|
|
63
|
+
return SourceExcerpt.dim("at #{entry}", ansi:) unless entry.is_a?(Entry) && module_ids.key?(entry.file)
|
|
64
|
+
|
|
65
|
+
"#{SourceExcerpt.dim("at", ansi:)} #{location(entry, module_ids, ansi:, links:)}#{SourceExcerpt.dim(":in '#{entry.description}'", ansi:)}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Graph modules are named by module ID and linked to the file they were
|
|
69
|
+
# evaluated from.
|
|
70
|
+
def location(entry, module_ids, ansi:, links:)
|
|
71
|
+
SourceExcerpt.styled_location(
|
|
72
|
+
module_id: module_ids.fetch(entry.file, entry.file),
|
|
73
|
+
line: entry.line,
|
|
74
|
+
path: entry.file,
|
|
75
|
+
ansi:,
|
|
76
|
+
links:
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -16,7 +16,8 @@ module Klenod
|
|
|
16
16
|
evaluated_module_ids = mods.keys
|
|
17
17
|
changed_module_ids = module_ids_for_paths(changed_paths)
|
|
18
18
|
removed_module_ids = module_ids_for_paths(removed_paths)
|
|
19
|
-
failed_retry_ids =
|
|
19
|
+
failed_retry_ids = failed_module_ids - removed_module_ids
|
|
20
|
+
imported_module_ids = records.each_key.select { |module_id| graph.dependents(module_id).any? }
|
|
20
21
|
pattern_owner_ids = module_ids_for_watched_paths(changed_paths + removed_paths)
|
|
21
22
|
plugin_owner_ids = plugin_invalidated_module_ids(changed_paths + removed_paths)
|
|
22
23
|
reload_module_ids = (changed_module_ids + failed_retry_ids + pattern_owner_ids + plugin_owner_ids).uniq
|
|
@@ -31,6 +32,12 @@ module Klenod
|
|
|
31
32
|
failed_reload_ids = []
|
|
32
33
|
reloaded_module_ids =
|
|
33
34
|
reload_module_ids.filter_map do |module_id|
|
|
35
|
+
if orphaned_failure?(module_id, imported_module_ids)
|
|
36
|
+
graph.remove_record(module_id)
|
|
37
|
+
mods.delete(module_id)
|
|
38
|
+
next
|
|
39
|
+
end
|
|
40
|
+
|
|
34
41
|
if evaluated_module_ids.include?(module_id)
|
|
35
42
|
graph.load_module(module_id, force: true)
|
|
36
43
|
else
|
|
@@ -124,28 +131,22 @@ module Klenod
|
|
|
124
131
|
end
|
|
125
132
|
end
|
|
126
133
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
unresolved_path = record.metadata.fetch(:error).unresolved_path if record.metadata.fetch(:error).respond_to?(:unresolved_path)
|
|
135
|
-
module_id if unresolved_path && relative_paths.any? { |path| path_satisfies_unresolved_path?(path, unresolved_path) }
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def relative_paths_for(paths)
|
|
140
|
-
paths.filter_map do |path|
|
|
141
|
-
Pathname.new(path).expand_path.relative_path_from(resolver.source_dir).to_s
|
|
142
|
-
rescue ArgumentError
|
|
143
|
-
nil
|
|
144
|
-
end
|
|
134
|
+
# A failed record keeps no dependency links, so nothing ties it to the file
|
|
135
|
+
# that broke it: a missing import, or a new dependency that fails to parse.
|
|
136
|
+
# Retry every failed module on each change. Failures are rare, and a retry
|
|
137
|
+
# that fails again reports its error again.
|
|
138
|
+
def failed_module_ids
|
|
139
|
+
records.filter_map { |module_id, record| module_id if record.status == :failed }
|
|
145
140
|
end
|
|
146
141
|
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
# A failed module whose last importer dropped it earlier in this update.
|
|
143
|
+
# Nothing can demand it any more, so retrying it would report its error
|
|
144
|
+
# on every change. Its record goes; importing it again collects it anew.
|
|
145
|
+
# A failed entry never had an importer and keeps being retried.
|
|
146
|
+
def orphaned_failure?(module_id, imported_module_ids)
|
|
147
|
+
records[module_id]&.status == :failed &&
|
|
148
|
+
imported_module_ids.include?(module_id) &&
|
|
149
|
+
graph.dependents(module_id).empty?
|
|
149
150
|
end
|
|
150
151
|
|
|
151
152
|
def plugin_invalidated_module_ids(paths)
|
data/lib/klenod/build/graph.rb
CHANGED
|
@@ -386,6 +386,18 @@ module Klenod
|
|
|
386
386
|
@mods[module_id] = mod
|
|
387
387
|
record
|
|
388
388
|
end
|
|
389
|
+
rescue SourceError => error
|
|
390
|
+
error.path ||= source_path_for(error.module_id)
|
|
391
|
+
raise
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# The file behind an app module, so error reports can link to it.
|
|
395
|
+
def source_path_for(module_id)
|
|
396
|
+
return nil unless module_id.is_a?(ModuleId) && module_id.scheme == :app
|
|
397
|
+
|
|
398
|
+
absolute_path(module_id).to_s
|
|
399
|
+
rescue ResolveError
|
|
400
|
+
nil
|
|
389
401
|
end
|
|
390
402
|
|
|
391
403
|
def collect_module(module_id, force: false)
|
|
@@ -443,6 +455,9 @@ module Klenod
|
|
|
443
455
|
@profiler.progress(:collect_module, module_id: record.id.to_s, total_records: @records.length)
|
|
444
456
|
record
|
|
445
457
|
end
|
|
458
|
+
rescue SourceError => error
|
|
459
|
+
error.path ||= source_path_for(error.module_id)
|
|
460
|
+
raise
|
|
446
461
|
end
|
|
447
462
|
|
|
448
463
|
def evaluate_module(module_id)
|
|
@@ -1106,6 +1106,15 @@
|
|
|
1106
1106
|
"unitsPerEm": 1000,
|
|
1107
1107
|
"xWidthAvg": 362
|
|
1108
1108
|
},
|
|
1109
|
+
"Asap Sharp": {
|
|
1110
|
+
"familyName": "Asap Sharp",
|
|
1111
|
+
"category": "sans-serif",
|
|
1112
|
+
"ascent": 934,
|
|
1113
|
+
"descent": -212,
|
|
1114
|
+
"lineGap": 0,
|
|
1115
|
+
"unitsPerEm": 1000,
|
|
1116
|
+
"xWidthAvg": 442
|
|
1117
|
+
},
|
|
1109
1118
|
"Asar": {
|
|
1110
1119
|
"familyName": "Asar",
|
|
1111
1120
|
"category": "serif",
|
|
@@ -2537,6 +2546,15 @@
|
|
|
2537
2546
|
"unitsPerEm": 16,
|
|
2538
2547
|
"xWidthAvg": 8
|
|
2539
2548
|
},
|
|
2549
|
+
"Caacupe One": {
|
|
2550
|
+
"familyName": "Caacupe One",
|
|
2551
|
+
"category": "display",
|
|
2552
|
+
"ascent": 960,
|
|
2553
|
+
"descent": -340,
|
|
2554
|
+
"lineGap": 0,
|
|
2555
|
+
"unitsPerEm": 1000,
|
|
2556
|
+
"xWidthAvg": 309
|
|
2557
|
+
},
|
|
2540
2558
|
"Cabin": {
|
|
2541
2559
|
"familyName": "Cabin",
|
|
2542
2560
|
"category": "sans-serif",
|
|
@@ -8187,7 +8205,7 @@
|
|
|
8187
8205
|
"descent": -120,
|
|
8188
8206
|
"lineGap": 0,
|
|
8189
8207
|
"unitsPerEm": 1000,
|
|
8190
|
-
"xWidthAvg":
|
|
8208
|
+
"xWidthAvg": 313
|
|
8191
8209
|
},
|
|
8192
8210
|
"Macondo": {
|
|
8193
8211
|
"familyName": "Macondo",
|
|
@@ -14993,6 +15011,15 @@
|
|
|
14993
15011
|
"unitsPerEm": 1000,
|
|
14994
15012
|
"xWidthAvg": 457
|
|
14995
15013
|
},
|
|
15014
|
+
"Scoutie Sans": {
|
|
15015
|
+
"familyName": "Scoutie Sans",
|
|
15016
|
+
"category": "sans-serif",
|
|
15017
|
+
"ascent": 2000,
|
|
15018
|
+
"descent": -600,
|
|
15019
|
+
"lineGap": 0,
|
|
15020
|
+
"unitsPerEm": 2000,
|
|
15021
|
+
"xWidthAvg": 920
|
|
15022
|
+
},
|
|
14996
15023
|
"Seaweed Script": {
|
|
14997
15024
|
"familyName": "Seaweed Script",
|
|
14998
15025
|
"category": "display",
|
|
@@ -16847,6 +16874,15 @@
|
|
|
16847
16874
|
"unitsPerEm": 1000,
|
|
16848
16875
|
"xWidthAvg": 400
|
|
16849
16876
|
},
|
|
16877
|
+
"Valley Sans": {
|
|
16878
|
+
"familyName": "Valley Sans",
|
|
16879
|
+
"category": "sans-serif",
|
|
16880
|
+
"ascent": 960,
|
|
16881
|
+
"descent": -240,
|
|
16882
|
+
"lineGap": 0,
|
|
16883
|
+
"unitsPerEm": 1000,
|
|
16884
|
+
"xWidthAvg": 487
|
|
16885
|
+
},
|
|
16850
16886
|
"Vampiro One": {
|
|
16851
16887
|
"familyName": "Vampiro One",
|
|
16852
16888
|
"category": "display",
|
|
@@ -5,7 +5,7 @@ Update them to the latest Capsize revision from the Klenod repository root:
|
|
|
5
5
|
bundle exec rake google_fonts:metrics:update
|
|
6
6
|
|
|
7
7
|
Project: https://github.com/seek-oss/capsize
|
|
8
|
-
Commit:
|
|
8
|
+
Commit: 8df97f098adc19d8a6d33df301757d7e9670bae6
|
|
9
9
|
Source: packages/metrics/src/entireMetricsCollection.json
|
|
10
10
|
|
|
11
11
|
MIT License
|
|
@@ -46,6 +46,7 @@ module Klenod
|
|
|
46
46
|
# invalid attribute list. Join the lines of an unclosed list first so
|
|
47
47
|
# both forms work across lines.
|
|
48
48
|
def parse_new_attributes(text)
|
|
49
|
+
tag_prefix = @line.text.strip[/\A[^(]*/]
|
|
49
50
|
joined_lines = 0
|
|
50
51
|
until (close_index = attribute_list_close_index(text)) || @next_line.eod?
|
|
51
52
|
text = "#{text} #{@next_line.text}"
|
|
@@ -56,7 +57,18 @@ module Klenod
|
|
|
56
57
|
pairs = close_index && extended_attribute_pairs(text[1...close_index])
|
|
57
58
|
rest = close_index && text[(close_index + 1)..]
|
|
58
59
|
unless pairs&.any? { |_name, value| value.match?(/[.\[]/) }
|
|
59
|
-
|
|
60
|
+
begin
|
|
61
|
+
attributes, rest, last_line = super
|
|
62
|
+
rescue ::Haml::SyntaxError => error
|
|
63
|
+
raise unless error.message.start_with?("Invalid attribute list")
|
|
64
|
+
|
|
65
|
+
# Haml quotes the list with String#inspect, which escapes every
|
|
66
|
+
# `"` in it. Show the source as written instead.
|
|
67
|
+
attribute_list = ::Haml::Util.balance(text, "(", ")")&.first || text
|
|
68
|
+
message = "Invalid attribute list: `#{attribute_list}`."
|
|
69
|
+
message = "#{message}\n\n#{hash_attributes_hint(tag_prefix, pairs)}" if pairs
|
|
70
|
+
raise ::Haml::SyntaxError.new(message, error.line)
|
|
71
|
+
end
|
|
60
72
|
return [attributes, rest, last_line + joined_lines]
|
|
61
73
|
end
|
|
62
74
|
|
|
@@ -147,6 +159,19 @@ module Klenod
|
|
|
147
159
|
pairs
|
|
148
160
|
end
|
|
149
161
|
|
|
162
|
+
# Every value is valid Ruby, but some are neither Haml's bare variables
|
|
163
|
+
# nor the member and index expressions handled above. Suggest the
|
|
164
|
+
# equivalent hash attributes, which accept any Ruby expression.
|
|
165
|
+
def hash_attributes_hint(tag_prefix, pairs)
|
|
166
|
+
entries = pairs.map do |name, value|
|
|
167
|
+
key = name.match?(/\A[a-z_]\w*\z/i) ? name : name.inspect
|
|
168
|
+
"#{key}: #{value}"
|
|
169
|
+
end
|
|
170
|
+
tag_prefix = "" unless tag_prefix.match?(/\A[%.#][-:\w.#]*\z/)
|
|
171
|
+
|
|
172
|
+
"Hint:\n Use {} attributes for Ruby expressions: #{tag_prefix}{#{entries.join(", ")}}"
|
|
173
|
+
end
|
|
174
|
+
|
|
150
175
|
# Returns the index of the `)` that closes the list opened at index 0,
|
|
151
176
|
# ignoring brackets inside quoted values.
|
|
152
177
|
def attribute_list_close_index(source)
|
|
@@ -1,34 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "errors"
|
|
4
|
+
require_relative "source_excerpt"
|
|
4
5
|
|
|
5
6
|
module Klenod
|
|
6
7
|
module Build
|
|
7
8
|
module ResolutionErrorFormatter
|
|
8
|
-
ANSI_BACKGROUND = "\e[0;48;5;52m"
|
|
9
|
-
|
|
10
9
|
module_function
|
|
11
10
|
|
|
12
11
|
def format(error, source_root: nil, source_context: nil, ansi: false)
|
|
13
12
|
return error.message unless error.resolution_failure?
|
|
14
13
|
|
|
15
|
-
lines = [error.title, "", " Import: #{error.requested_specifier}"]
|
|
14
|
+
lines = [SourceExcerpt.header(error.title, "", ansi:), "", " Import: #{error.requested_specifier}"]
|
|
16
15
|
lines << " Imported by: #{error.imported_by}" if error.imported_by
|
|
17
16
|
lines << " Source root: #{source_root}" if source_root
|
|
18
17
|
append_suggestions(lines, error)
|
|
19
18
|
lines.concat(["", source_context]) if source_context
|
|
20
|
-
|
|
21
|
-
ansi ? format_ansi(text) : text
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def format_ansi(text)
|
|
25
|
-
title, separator, body = text.partition("\n")
|
|
26
|
-
body = body.gsub("\e[0m", ANSI_BACKGROUND)
|
|
27
|
-
body = "\n#{body}" unless separator.empty?
|
|
28
|
-
|
|
29
|
-
"\e[1;31;47m ERROR \e[1;37;41m #{title} #{ANSI_BACKGROUND}#{body}\e[0m"
|
|
19
|
+
lines.join("\n")
|
|
30
20
|
end
|
|
31
|
-
private_class_method :format_ansi
|
|
32
21
|
|
|
33
22
|
def append_suggestions(lines, error)
|
|
34
23
|
return if error.suggestions.empty?
|
|
@@ -27,7 +27,7 @@ module Klenod
|
|
|
27
27
|
def initialize(line: nil, column: nil, detail: nil, hints: []) = super
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
attr_reader :module_id, :source, :line, :column, :cause, :detail, :hints
|
|
30
|
+
attr_reader :module_id, :source, :line, :column, :cause, :detail, :hints, :path
|
|
31
31
|
|
|
32
32
|
def initialize(error, source:, module_id:)
|
|
33
33
|
@cause = error
|
|
@@ -40,19 +40,31 @@ module Klenod
|
|
|
40
40
|
@detail = found.detail || message_for(error)
|
|
41
41
|
@hints = Array(found.hints).map(&:to_s).reject(&:empty?).freeze
|
|
42
42
|
|
|
43
|
-
super(
|
|
43
|
+
super(@detail)
|
|
44
|
+
|
|
45
|
+
set_backtrace(backtrace_for(error))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The file the module was read from, when it has one. The graph sets it
|
|
49
|
+
# as the error leaves the module, since plugins only know the module ID,
|
|
50
|
+
# and the message links the location to it.
|
|
51
|
+
def path=(path)
|
|
52
|
+
@path = path
|
|
53
|
+
@message = nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_s
|
|
57
|
+
@message ||=
|
|
44
58
|
SourceExcerpt.message(
|
|
45
59
|
module_id:,
|
|
46
|
-
line
|
|
47
|
-
column
|
|
60
|
+
line:,
|
|
61
|
+
column:,
|
|
48
62
|
kind:,
|
|
49
63
|
source:,
|
|
50
|
-
message:
|
|
51
|
-
hints
|
|
64
|
+
message: detail,
|
|
65
|
+
hints:,
|
|
66
|
+
path:
|
|
52
67
|
)
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
set_backtrace(backtrace_for(error))
|
|
56
68
|
end
|
|
57
69
|
|
|
58
70
|
# "JavaScript parse error". Used as the label in the browser error dialog,
|
|
@@ -10,34 +10,120 @@ module Klenod
|
|
|
10
10
|
# `ansi: false` to get the same layout without escape codes.
|
|
11
11
|
module SourceExcerpt
|
|
12
12
|
MARKED_LINE = "\e[1;31m"
|
|
13
|
+
KIND = "\e[1;31m"
|
|
14
|
+
DETAIL = "\e[31m"
|
|
15
|
+
HINT_LABEL = "\e[1;36m"
|
|
16
|
+
HINT = "\e[36m"
|
|
17
|
+
LOCATION = "\e[34m"
|
|
18
|
+
BOLD = "\e[1m"
|
|
19
|
+
NOT_BOLD = "\e[22m"
|
|
20
|
+
DIM = "\e[2m"
|
|
13
21
|
RESET = "\e[0m"
|
|
14
22
|
|
|
15
23
|
module_function
|
|
16
24
|
|
|
17
|
-
# "app:/pages/thing.tsx:33:9
|
|
18
|
-
def
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
# "app:/pages/thing.tsx:33:9"
|
|
26
|
+
def location(module_id:, line:, column: nil)
|
|
27
|
+
if module_id && line
|
|
28
|
+
"#{module_id}:#{[line, column].compact.join(":")}"
|
|
29
|
+
elsif module_id
|
|
30
|
+
module_id.to_s
|
|
31
|
+
elsif line
|
|
32
|
+
column ? "line #{line} column #{column}" : "line #{line}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
# The kind and detail share a header line. The location goes in the
|
|
37
|
+
# excerpt's frame, or on an "at" line when there is no excerpt, and the
|
|
38
|
+
# hints follow:
|
|
39
|
+
#
|
|
40
|
+
# × Haml parse error: Could not parse Ruby filter
|
|
41
|
+
#
|
|
42
|
+
# ╭─[app:/pages/page.haml:4]
|
|
43
|
+
# 3 │ @count = 0
|
|
44
|
+
# > 4 │
|
|
45
|
+
# ╰────
|
|
46
|
+
# hint: Unmatched keyword, missing `end' ?
|
|
47
|
+
#
|
|
48
|
+
# With `path`, the file the module was read from, the location links to
|
|
49
|
+
# that file when `links` is on.
|
|
50
|
+
def message(module_id:, line:, kind:, source:, message:, column: nil, hints: [], context: 2, ansi: true, path: nil, links: ansi && hyperlinks?)
|
|
51
|
+
location = styled_location(module_id:, line:, column:, path:, ansi:, links:)
|
|
52
|
+
excerpt = excerpt(source:, line:, column:, context:, ansi:, location:)
|
|
53
|
+
hints = hint_section(hints, ansi:)&.gsub(/^/, " ")
|
|
54
|
+
header = header(kind, message, ansi:)
|
|
55
|
+
|
|
56
|
+
unless excerpt
|
|
57
|
+
header = "#{header}\n #{dim("at", ansi:)} #{location}" if location
|
|
58
|
+
return [header, hints].compact.join("\n\n")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
[header, [excerpt, hints].compact.join("\n")].join("\n\n")
|
|
29
62
|
end
|
|
30
63
|
|
|
31
|
-
|
|
64
|
+
# "× JSON parse error: expected object key". Later lines of a multi-line
|
|
65
|
+
# detail are indented under the first.
|
|
66
|
+
def header(kind, message, ansi: true)
|
|
67
|
+
first, *rest = message.to_s.lines(chomp: true)
|
|
68
|
+
label = first ? "× #{kind}:" : "× #{kind}"
|
|
69
|
+
label = "#{KIND}#{label}#{RESET}" if ansi
|
|
70
|
+
return label unless first
|
|
71
|
+
|
|
32
72
|
[
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
73
|
+
"#{label} #{detail(first, ansi:)}",
|
|
74
|
+
*rest.map { it.empty? ? it : " #{detail(it, ansi:)}" }
|
|
75
|
+
].join("\n")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The location in blue with the file name in bold, linked to `path` when
|
|
79
|
+
# `links` is on and the file exists.
|
|
80
|
+
def styled_location(module_id:, line: nil, column: nil, path: nil, ansi: true, links: ansi && hyperlinks?)
|
|
81
|
+
location = location(module_id:, line:, column:)
|
|
82
|
+
return location unless ansi && location
|
|
83
|
+
|
|
84
|
+
location = bold_file_name(location, module_id) if module_id
|
|
85
|
+
location = file_link(location, path:, line:, column:) if links && path && File.file?(path.to_s)
|
|
86
|
+
"#{LOCATION}#{location}#{RESET}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# "app:\e[1m/pages/thing.tsx\e[22m:33:9": the file name stands out from its
|
|
90
|
+
# scheme and position.
|
|
91
|
+
def bold_file_name(location, module_id)
|
|
92
|
+
scheme, name = module_id.to_s.match(/\A([a-z][a-z0-9+.-]*:)?(.*)\z/i).captures
|
|
93
|
+
location.sub(module_id.to_s) { "#{scheme}#{BOLD}#{name}#{NOT_BOLD}" }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Whether the terminal turns OSC 8 sequences into clickable links. There
|
|
97
|
+
# is no way to ask, so this recognises the terminals known to do it.
|
|
98
|
+
def hyperlinks?(env = ENV)
|
|
99
|
+
return false if env.key?("NO_COLOR")
|
|
100
|
+
return true if %w[iTerm.app WezTerm vscode ghostty Hyper Tabby rio].include?(env["TERM_PROGRAM"])
|
|
101
|
+
return true if %w[xterm-kitty foot contour].include?(env["TERM"])
|
|
102
|
+
return true if env.key?("KITTY_WINDOW_ID") || env.key?("WT_SESSION") || env.key?("ALACRITTY_WINDOW_ID")
|
|
103
|
+
return true if env["VTE_VERSION"].to_i >= 5000
|
|
104
|
+
return true if env["KONSOLE_VERSION"].to_i >= 201200
|
|
105
|
+
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Wraps `text` in an OSC 8 hyperlink to `path`, with the line and column
|
|
110
|
+
# as the fragment: "file:///app/pages/thing.tsx#33:9".
|
|
111
|
+
def file_link(text, path:, line: nil, column: nil)
|
|
112
|
+
encoded = path.to_s.b.gsub(%r{[^A-Za-z0-9\-._~/]}) { format("%%%02X", it.ord) }
|
|
113
|
+
fragment = line && "##{[line, column].compact.join(":")}"
|
|
114
|
+
|
|
115
|
+
"\e]8;;file://#{encoded}#{fragment}\e\\#{text}\e]8;;\e\\"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Colored per line so loggers that prefix each line do not carry the
|
|
119
|
+
# color into their gutter.
|
|
120
|
+
def detail(message, ansi: true)
|
|
121
|
+
return message unless ansi && message
|
|
122
|
+
|
|
123
|
+
message.to_s.lines(chomp: true).map { it.empty? ? it : "#{DETAIL}#{it}#{RESET}" }.join("\n")
|
|
38
124
|
end
|
|
39
125
|
|
|
40
|
-
def excerpt(source:, line:, column: nil, context: 2, ansi: true)
|
|
126
|
+
def excerpt(source:, line:, column: nil, context: 2, ansi: true, location: nil)
|
|
41
127
|
return nil unless line
|
|
42
128
|
|
|
43
129
|
lines = source.to_s.lines
|
|
@@ -49,35 +135,53 @@ module Klenod
|
|
|
49
135
|
first = [index - context, 0].max
|
|
50
136
|
last = [index + context, lines.length - 1].min
|
|
51
137
|
width = (last + 1).to_s.length
|
|
138
|
+
gutter = " " * (width + 3)
|
|
52
139
|
|
|
53
140
|
rows =
|
|
54
141
|
(first..last).flat_map do |line_index|
|
|
55
142
|
marked = line_index == index
|
|
56
143
|
number = (line_index + 1).to_s.rjust(width)
|
|
57
|
-
|
|
58
|
-
formatted =
|
|
59
|
-
|
|
60
|
-
|
|
144
|
+
code = lines.fetch(line_index).chomp
|
|
145
|
+
formatted =
|
|
146
|
+
if marked
|
|
147
|
+
row = "> #{number} │ #{code}".rstrip
|
|
148
|
+
ansi ? "#{MARKED_LINE}#{row}#{RESET}" : row
|
|
149
|
+
else
|
|
150
|
+
"#{dim(" #{number} │", ansi:)} #{code}".rstrip
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
marked ? [formatted, caret_row(width, column, ansi:)].compact : [formatted]
|
|
61
154
|
end
|
|
62
155
|
|
|
63
|
-
|
|
156
|
+
[
|
|
157
|
+
location ? "#{gutter}#{dim("╭─[", ansi:)}#{location}#{dim("]", ansi:)}" : "#{gutter}#{dim("╭────", ansi:)}",
|
|
158
|
+
*rows,
|
|
159
|
+
"#{gutter}#{dim("╰────", ansi:)}"
|
|
160
|
+
].join("\n")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# `text` without color codes or OSC 8 links, as a terminal would show it.
|
|
164
|
+
def strip(text)
|
|
165
|
+
text.to_s.gsub(/\e\[[0-9;]*m|\e\]8;[^\e\a]*(?:\e\\|\a)/, "")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def dim(text, ansi: true)
|
|
169
|
+
ansi ? "#{DIM}#{text}#{RESET}" : text
|
|
64
170
|
end
|
|
65
171
|
|
|
66
|
-
# "
|
|
67
|
-
def caret_row(width, column)
|
|
172
|
+
# " · ^", aligned under the offending column of the marked line.
|
|
173
|
+
def caret_row(width, column, ansi: true)
|
|
68
174
|
return nil unless column&.positive?
|
|
69
175
|
|
|
70
|
-
"#{" " * (width + 3)}
|
|
176
|
+
"#{" " * (width + 3)}#{dim("·", ansi:)} #{" " * (column - 1)}^"
|
|
71
177
|
end
|
|
72
178
|
|
|
73
|
-
# What to try next
|
|
74
|
-
|
|
75
|
-
def hint_section(hints)
|
|
179
|
+
# What to try next, one "hint: ..." line per hint.
|
|
180
|
+
def hint_section(hints, ansi: true)
|
|
76
181
|
hints = Array(hints).map(&:to_s).reject(&:empty?)
|
|
77
182
|
return nil if hints.empty?
|
|
78
183
|
|
|
79
|
-
|
|
80
|
-
"#{heading}\n#{hints.map { " #{it}" }.join("\n")}"
|
|
184
|
+
hints.map { ansi ? "#{HINT_LABEL}hint:#{RESET} #{HINT}#{it}#{RESET}" : "hint: #{it}" }.join("\n")
|
|
81
185
|
end
|
|
82
186
|
end
|
|
83
187
|
end
|
data/lib/klenod/build/version.rb
CHANGED
data/lib/klenod/build.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "build/version"
|
|
4
4
|
require "klenod/runtime"
|
|
5
5
|
require_relative "build/context"
|
|
6
|
+
require_relative "build/exception_formatter"
|
|
7
|
+
require_relative "build/resolution_error_formatter"
|
|
6
8
|
|
|
7
9
|
module Klenod
|
|
8
10
|
class Error < StandardError; end unless const_defined?(:Error, false)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: klenod-build
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.26
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrés Alin
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.26
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.26
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: async
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -257,6 +257,7 @@ files:
|
|
|
257
257
|
- lib/klenod/build/context.rb
|
|
258
258
|
- lib/klenod/build/dependency.rb
|
|
259
259
|
- lib/klenod/build/errors.rb
|
|
260
|
+
- lib/klenod/build/exception_formatter.rb
|
|
260
261
|
- lib/klenod/build/filesystem_resolver.rb
|
|
261
262
|
- lib/klenod/build/graph.rb
|
|
262
263
|
- lib/klenod/build/graph/invalidator.rb
|