klenod-build 0.0.16 → 0.0.18
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/graph.rb +43 -8
- data/lib/klenod/build/plugins/class_names_runtime.rb +1 -1
- data/lib/klenod/build/plugins/haml_plugin/helper_source.rb +2 -1
- data/lib/klenod/build/plugins/haml_plugin/parser.rb +67 -0
- data/lib/klenod/build/plugins/haml_plugin/transformer/ruby_builder.rb +11 -7
- data/lib/klenod/build/plugins/haml_plugin/transformer.rb +3 -0
- data/lib/klenod/build/plugins/haml_plugin.rb +16 -7
- data/lib/klenod/build/plugins/markdown_plugin.rb +2 -2
- data/lib/klenod/build/plugins/ruby_plugin.rb +64 -8
- data/lib/klenod/build/ruby_import_rewriter.rb +37 -15
- data/lib/klenod/build/source_error.rb +37 -0
- data/lib/klenod/build/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b36ee8205987d239a6301d55da7f4ef92548303139d04b5bbc2246ba6e94e8e3
|
|
4
|
+
data.tar.gz: 8f6db6e13f72e4b290b91d115df9e38e8e907100db677baea5a5fec047bc6ae0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9572bdc8295bfa42c28a88057e6625d1b105cfb8b7c9f51bc7d426ffb0e090c472237f86f549258da64b1095931921e5259a78a41050590b4d67254c92590c5
|
|
7
|
+
data.tar.gz: e4b49528fe297b00c61d9f45bd8b780e88d485c9c3c4e1f37c1a28ce63ee128af0c64bccdaa68567197f0aa5beae51f568fd4371cb4d4a273c82ca7204a73b19
|
data/lib/klenod/build/graph.rb
CHANGED
|
@@ -374,6 +374,7 @@ module Klenod
|
|
|
374
374
|
transform = loaded_source.transform || transform_module_source(module_id, source)
|
|
375
375
|
resolved_dependencies = resolve_transform_dependencies(module_id, transform)
|
|
376
376
|
dependency_records = load_eager_dependency_records(resolved_dependencies)
|
|
377
|
+
assert_eager_import_names!(module_id, source, resolved_dependencies, dependency_records)
|
|
377
378
|
transform = finalize_transform_result(module_id, transform, resolved_dependencies, dependency_records)
|
|
378
379
|
assert_supported_transform!(module_id, source, transform)
|
|
379
380
|
assert_generated_ruby_parses!(module_id, transform)
|
|
@@ -430,6 +431,7 @@ module Klenod
|
|
|
430
431
|
transform = loaded_source.transform || transform_module_source(module_id, source)
|
|
431
432
|
resolved_dependencies = resolve_transform_dependencies(module_id, transform)
|
|
432
433
|
dependency_records = collect_eager_dependency_records(resolved_dependencies)
|
|
434
|
+
assert_eager_import_names!(module_id, source, resolved_dependencies, dependency_records)
|
|
433
435
|
transform = finalize_transform_result(module_id, transform, resolved_dependencies, dependency_records)
|
|
434
436
|
assert_supported_transform!(module_id, source, transform)
|
|
435
437
|
assert_generated_ruby_parses!(module_id, transform)
|
|
@@ -462,7 +464,7 @@ module Klenod
|
|
|
462
464
|
Runtime::Mod.new(
|
|
463
465
|
module_id.to_s,
|
|
464
466
|
record.transformed_source,
|
|
465
|
-
imports: imports_for(record.resolved_dependencies, dependency_records),
|
|
467
|
+
imports: imports_for(module_id, record.source, record.resolved_dependencies, dependency_records),
|
|
466
468
|
source_map: record.source_map,
|
|
467
469
|
version: record.version,
|
|
468
470
|
eval_path: eval_path_for(module_id),
|
|
@@ -554,7 +556,11 @@ module Klenod
|
|
|
554
556
|
record
|
|
555
557
|
.resolved_dependencies
|
|
556
558
|
.reject { skip_asset_dependency?(it, type:) }
|
|
557
|
-
|
|
559
|
+
if type == :css
|
|
560
|
+
regular_dependencies, companion_styles = dependency_ids.partition { |dependency| dependency.dependency.kind != :companion_style }
|
|
561
|
+
dependency_ids = [*regular_dependencies, *companion_styles]
|
|
562
|
+
end
|
|
563
|
+
dependency_ids = dependency_ids.map(&:module_id)
|
|
558
564
|
|
|
559
565
|
if module_id.extname == ".css"
|
|
560
566
|
dependency_ids.flat_map { |dependency_id| ordered_module_ids_for_assets(dependency_id, seen, type: type) } + [module_id]
|
|
@@ -659,7 +665,7 @@ module Klenod
|
|
|
659
665
|
Runtime::Mod.new(
|
|
660
666
|
module_id.to_s,
|
|
661
667
|
transform.code,
|
|
662
|
-
imports: imports_for(resolved_dependencies, dependency_records),
|
|
668
|
+
imports: imports_for(module_id, source, resolved_dependencies, dependency_records),
|
|
663
669
|
source_map: transform.source_map,
|
|
664
670
|
version: cached ? cached.version + 1 : 0,
|
|
665
671
|
eval_path: eval_path_for(module_id),
|
|
@@ -690,7 +696,7 @@ module Klenod
|
|
|
690
696
|
source_dir.join(module_id.path).to_s
|
|
691
697
|
end
|
|
692
698
|
|
|
693
|
-
def imports_for(resolved_dependencies, dependency_records)
|
|
699
|
+
def imports_for(module_id, source, resolved_dependencies, dependency_records)
|
|
694
700
|
resolved_dependencies.to_h do |resolved_dependency|
|
|
695
701
|
value =
|
|
696
702
|
if resolved_dependency.dependency.eager
|
|
@@ -702,6 +708,7 @@ module Klenod
|
|
|
702
708
|
evaluate_module(resolved_dependency.module_id)
|
|
703
709
|
record = @records.fetch(resolved_dependency.module_id)
|
|
704
710
|
raise_failed_module!(record)
|
|
711
|
+
assert_import_name!(module_id, source, resolved_dependency, record)
|
|
705
712
|
import_value(resolved_dependency, record)
|
|
706
713
|
end
|
|
707
714
|
end
|
|
@@ -898,10 +905,9 @@ module Klenod
|
|
|
898
905
|
@records.to_h do |module_id, record|
|
|
899
906
|
imports =
|
|
900
907
|
record.resolved_dependencies.to_h do |resolved_dependency|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
]
|
|
908
|
+
target = @records.fetch(resolved_dependency.module_id)
|
|
909
|
+
assert_import_name!(module_id, record.source, resolved_dependency, target)
|
|
910
|
+
[resolved_dependency.dependency.id, runtime_import_spec(resolved_dependency, target)]
|
|
905
911
|
end
|
|
906
912
|
|
|
907
913
|
[
|
|
@@ -990,6 +996,35 @@ module Klenod
|
|
|
990
996
|
end
|
|
991
997
|
end
|
|
992
998
|
|
|
999
|
+
# Lazy targets are not collected until bundle or call time, so only the
|
|
1000
|
+
# eager imports can be checked while the importer is collected.
|
|
1001
|
+
def assert_eager_import_names!(module_id, source, resolved_dependencies, dependency_records)
|
|
1002
|
+
resolved_dependencies.each do |resolved_dependency|
|
|
1003
|
+
next unless resolved_dependency.dependency.eager
|
|
1004
|
+
|
|
1005
|
+
record = dependency_records.fetch(resolved_dependency.dependency.id)
|
|
1006
|
+
assert_import_name!(module_id, source, resolved_dependency, record)
|
|
1007
|
+
end
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
# A named import (`import("./x", :Bar)`) is checked against the constants
|
|
1011
|
+
# the target defines statically, recorded by the Ruby plugin as
|
|
1012
|
+
# metadata[:ruby_constants]. A missing Default is not an error: the
|
|
1013
|
+
# import then returns the Exports module.
|
|
1014
|
+
def assert_import_name!(module_id, source, resolved_dependency, record)
|
|
1015
|
+
name = resolved_dependency.dependency.metadata[:import_name]
|
|
1016
|
+
return unless name
|
|
1017
|
+
|
|
1018
|
+
constants = record.metadata[:ruby_constants]
|
|
1019
|
+
return if constants&.include?(name)
|
|
1020
|
+
|
|
1021
|
+
raise MissingExportError.new(
|
|
1022
|
+
MissingExportError::Report.new(resolved_dependency.dependency, record.id, name, constants),
|
|
1023
|
+
source: source,
|
|
1024
|
+
module_id: module_id
|
|
1025
|
+
)
|
|
1026
|
+
end
|
|
1027
|
+
|
|
993
1028
|
def import_value(resolved_dependency, record)
|
|
994
1029
|
raise_failed_module!(record)
|
|
995
1030
|
|
|
@@ -22,7 +22,7 @@ module Klenod
|
|
|
22
22
|
return nil unless module_id.scheme == :virtual && module_id == CLASS_NAMES_MODULE_ID
|
|
23
23
|
|
|
24
24
|
source = class_names_runtime_source
|
|
25
|
-
LoadResult.new(source, nil, TransformResult.new(source, [], nil, [], [], {}))
|
|
25
|
+
LoadResult.new(source, nil, TransformResult.new(source, [], nil, [], [], {ruby_constants: [:Default]}))
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def class_names_runtime_dependency(module_id)
|
|
@@ -35,6 +35,20 @@ module Klenod
|
|
|
35
35
|
result
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Haml's parenthesized-attribute parser accepts only a bare variable
|
|
39
|
+
# after `=`. For example, it reads `video_id=video.id` as the value
|
|
40
|
+
# `video` plus a `.id` class. Extend that form to ordinary Ruby
|
|
41
|
+
# member and index expressions while leaving all other attributes on
|
|
42
|
+
# Haml's native parsing path.
|
|
43
|
+
def parse_new_attributes(text)
|
|
44
|
+
balanced, rest = ::Haml::Util.balance(text, "(", ")")
|
|
45
|
+
pairs = balanced && extended_attribute_pairs(balanced[1...-1])
|
|
46
|
+
return super unless pairs&.any? { |_name, value| value.match?(/[.\[]/) }
|
|
47
|
+
|
|
48
|
+
dynamic = pairs.reduce("{") { |source, (name, value)| "#{source}#{::Haml::Util.inspect_obj(name)} => #{value}," } << "}"
|
|
49
|
+
[[{}, dynamic], rest, @line.index + 1]
|
|
50
|
+
end
|
|
51
|
+
|
|
38
52
|
def annotate_tag_nodes(root)
|
|
39
53
|
queue = root.children.dup
|
|
40
54
|
until queue.empty?
|
|
@@ -62,6 +76,59 @@ module Klenod
|
|
|
62
76
|
{shorthand: shorthand, literal: literal}
|
|
63
77
|
end
|
|
64
78
|
|
|
79
|
+
def extended_attribute_pairs(source)
|
|
80
|
+
pairs = []
|
|
81
|
+
index = 0
|
|
82
|
+
|
|
83
|
+
loop do
|
|
84
|
+
index += 1 while source[index]&.match?(/\s/)
|
|
85
|
+
break if index >= source.length
|
|
86
|
+
|
|
87
|
+
name = source[index..].match(/\A[-:@#\w.]+/)&.to_s
|
|
88
|
+
return nil unless name
|
|
89
|
+
|
|
90
|
+
index += name.length
|
|
91
|
+
index += 1 while source[index]&.match?(/\s/)
|
|
92
|
+
return nil unless source[index] == "="
|
|
93
|
+
|
|
94
|
+
index += 1
|
|
95
|
+
index += 1 while source[index]&.match?(/\s/)
|
|
96
|
+
value_start = index
|
|
97
|
+
depth = 0
|
|
98
|
+
quote = nil
|
|
99
|
+
escaped = false
|
|
100
|
+
|
|
101
|
+
while index < source.length
|
|
102
|
+
character = source[index]
|
|
103
|
+
if quote
|
|
104
|
+
if escaped
|
|
105
|
+
escaped = false
|
|
106
|
+
elsif character == "\\"
|
|
107
|
+
escaped = true
|
|
108
|
+
elsif character == quote
|
|
109
|
+
quote = nil
|
|
110
|
+
end
|
|
111
|
+
else
|
|
112
|
+
case character
|
|
113
|
+
when "'", '"' then quote = character
|
|
114
|
+
when "(", "[", "{" then depth += 1
|
|
115
|
+
when ")", "]", "}" then depth -= 1
|
|
116
|
+
when " ", "\t"
|
|
117
|
+
break if depth.zero? && source[index..].match?(/\A\s+[-:@#\w.]+\s*=/)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
index += 1
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
value = source[value_start...index].strip
|
|
124
|
+
return nil if value.empty? || depth.negative? || quote
|
|
125
|
+
|
|
126
|
+
pairs << [name, value]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
pairs
|
|
130
|
+
end
|
|
131
|
+
|
|
65
132
|
def literal_class_names_from_old_attributes(source)
|
|
66
133
|
parsed = ::Haml::AttributeParser.parse(source)
|
|
67
134
|
return [] unless parsed&.key?("class")
|
|
@@ -489,7 +489,9 @@ module Klenod
|
|
|
489
489
|
def render_ruby_filter(node)
|
|
490
490
|
source = to_source(node)
|
|
491
491
|
parsed = (node if node.is_a?(Fragment) && node.node?) || statements(source)
|
|
492
|
-
|
|
492
|
+
unless parsed&.node?
|
|
493
|
+
raise_ruby_parse_error(source, line_no: nil, context: "Could not parse Ruby filter")
|
|
494
|
+
end
|
|
493
495
|
|
|
494
496
|
fragment(
|
|
495
497
|
ast_begin([
|
|
@@ -497,8 +499,6 @@ module Klenod
|
|
|
497
499
|
nil_node
|
|
498
500
|
])
|
|
499
501
|
)
|
|
500
|
-
rescue SyntaxTree::Parser::ParseError
|
|
501
|
-
statements("begin\n#{indent(source, 2)}\n nil\nend")
|
|
502
502
|
end
|
|
503
503
|
|
|
504
504
|
def format_node(node)
|
|
@@ -575,16 +575,20 @@ module Klenod
|
|
|
575
575
|
line_offsets = [0]
|
|
576
576
|
source.each_line(chomp: false) { |line| line_offsets << line_offsets.last + line.length }
|
|
577
577
|
|
|
578
|
-
Ripper
|
|
579
|
-
|
|
580
|
-
|
|
578
|
+
tokens = Ripper.lex(source)
|
|
579
|
+
|
|
580
|
+
tokens
|
|
581
|
+
.each_with_index
|
|
582
|
+
.filter_map do |((line, column), type, token, _state), index|
|
|
581
583
|
kind, prefix, pattern = VARIABLE_TOKEN_KINDS[type]
|
|
582
584
|
receiver = @variables[kind]
|
|
583
585
|
if type == :on_gvar && token == "$*" && receiver
|
|
584
586
|
[offset_for(line_offsets, line, column), token.length, receiver]
|
|
585
587
|
elsif receiver && token.match?(pattern)
|
|
586
588
|
name = token.delete_prefix(prefix)
|
|
587
|
-
|
|
589
|
+
replacement = "(#{receiver})[#{symbol_source(name)}]"
|
|
590
|
+
replacement = "{#{replacement}}" if tokens[index - 1]&.fetch(1) == :on_embvar
|
|
591
|
+
[offset_for(line_offsets, line, column), token.length, replacement]
|
|
588
592
|
end
|
|
589
593
|
end
|
|
590
594
|
.reverse_each
|
|
@@ -549,6 +549,9 @@ module Klenod
|
|
|
549
549
|
source_column_offset = source.lines.fetch(node.line, "").match(/\A\s*/).to_s.length
|
|
550
550
|
text = import_rewriter.call(text, source_line_offset: node.line, source_column_offset: source_column_offset)
|
|
551
551
|
end
|
|
552
|
+
unless builder.statements(text).node?
|
|
553
|
+
builder.ruby_parse_error(text, line_no: node.line + 1, context: "Could not parse Ruby filter")
|
|
554
|
+
end
|
|
552
555
|
heredoc_body_lines = ruby_heredoc_body_lines(text)
|
|
553
556
|
source = +""
|
|
554
557
|
text.each_line.with_index(node.line + 1) do |line, line_no|
|
|
@@ -97,7 +97,9 @@ module Klenod
|
|
|
97
97
|
|
|
98
98
|
return nil unless module_id.scheme == :virtual && module_id == HAML_HELPER_MODULE_ID
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
# Loaded with a ready transform, so the Ruby plugin never scans it: declare
|
|
101
|
+
# the Default export it defines.
|
|
102
|
+
LoadResult.new(haml_helper_source, nil, TransformResult.new(haml_helper_source, [], nil, [], [], {ruby_constants: [:Default]}))
|
|
101
103
|
end
|
|
102
104
|
|
|
103
105
|
def transform(module_id, code, context)
|
|
@@ -136,7 +138,7 @@ module Klenod
|
|
|
136
138
|
specifier: "/markdown-components",
|
|
137
139
|
importer_id: module_id,
|
|
138
140
|
kind: :markdown_components,
|
|
139
|
-
metadata: {optional: true}
|
|
141
|
+
metadata: {optional: true, import_name: :Default}
|
|
140
142
|
)
|
|
141
143
|
.with(id: "#{module_id}:markdown_components")
|
|
142
144
|
dependencies << markdown_components_dependency
|
|
@@ -205,11 +207,11 @@ module Klenod
|
|
|
205
207
|
styles_source: styles_source,
|
|
206
208
|
translations_source: translations_source,
|
|
207
209
|
i18n_source: i18n_source_for(builder),
|
|
208
|
-
haml_helper_source: haml_helper_dependency && builder.constant_assignment("HamlHelper",
|
|
210
|
+
haml_helper_source: haml_helper_dependency && builder.constant_assignment("HamlHelper", builder.import_call(haml_helper_dependency.id).source),
|
|
209
211
|
styleable: !style_dependencies.empty?,
|
|
210
212
|
profiler: context.profiler,
|
|
211
213
|
import_rewriter: import_rewriter,
|
|
212
|
-
markdown_components_source: markdown_components_dependency ? "__klenod_import__(#{markdown_components_dependency.id.inspect})
|
|
214
|
+
markdown_components_source: markdown_components_dependency ? "__klenod_import__(#{markdown_components_dependency.id.inspect})" : "{}",
|
|
213
215
|
variables: @variables,
|
|
214
216
|
event_handler: @event_handler,
|
|
215
217
|
cache_static_subtrees: @cache_static_subtrees
|
|
@@ -246,19 +248,26 @@ module Klenod
|
|
|
246
248
|
def import_value(resolved_dependency, record, context)
|
|
247
249
|
styles_import = class_names_runtime_import_value(resolved_dependency, record, context)
|
|
248
250
|
return styles_import if styles_import
|
|
249
|
-
return nil unless record
|
|
251
|
+
return nil unless default_export?(record)
|
|
250
252
|
|
|
251
|
-
context.mods.fetch(record.id).const_get(:Exports)
|
|
253
|
+
context.mods.fetch(record.id).const_get(:Exports).const_get(:Default, false)
|
|
252
254
|
end
|
|
253
255
|
|
|
254
256
|
def runtime_import_value(resolved_dependency, record, _context)
|
|
255
257
|
styles_import = class_names_runtime_runtime_import_value(resolved_dependency, record)
|
|
256
258
|
return styles_import if styles_import
|
|
257
|
-
return Runtime::DefaultImport.new(:Default) if record
|
|
259
|
+
return Runtime::DefaultImport.new(:Default) if default_export?(record)
|
|
258
260
|
|
|
259
261
|
super
|
|
260
262
|
end
|
|
261
263
|
|
|
264
|
+
# Components and the helper module both export Default. The helper is
|
|
265
|
+
# unwrapped here as well as by the Ruby plugin, so a context built
|
|
266
|
+
# without the Ruby plugin still renders.
|
|
267
|
+
def default_export?(record)
|
|
268
|
+
record.id.extname == ".haml" || record.id == HAML_HELPER_MODULE_ID
|
|
269
|
+
end
|
|
270
|
+
|
|
262
271
|
def invalidate_module_ids(paths, context)
|
|
263
272
|
paths
|
|
264
273
|
.filter_map { |path| companion_owner_module_id(path, context) }
|
|
@@ -168,13 +168,13 @@ module Klenod
|
|
|
168
168
|
specifier: "/markdown-components",
|
|
169
169
|
importer_id: module_id,
|
|
170
170
|
kind: :markdown_components,
|
|
171
|
-
metadata: {optional: true}
|
|
171
|
+
metadata: {optional: true, import_name: :Default}
|
|
172
172
|
)
|
|
173
173
|
.with(id: dependency_id(module_id))
|
|
174
174
|
end
|
|
175
175
|
|
|
176
176
|
def components_source_for(dependency)
|
|
177
|
-
dependency ? "__klenod_import__(#{dependency.id.inspect})
|
|
177
|
+
dependency ? "__klenod_import__(#{dependency.id.inspect})" : "{}"
|
|
178
178
|
end
|
|
179
179
|
|
|
180
180
|
def dependency_id(module_id)
|
|
@@ -53,7 +53,7 @@ module Klenod
|
|
|
53
53
|
def transform(module_id, code, context)
|
|
54
54
|
return TransformResult.identity(code) unless module_id.extname == ".rb"
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
program = parse!(module_id, code)
|
|
57
57
|
|
|
58
58
|
result =
|
|
59
59
|
begin
|
|
@@ -68,21 +68,77 @@ module Klenod
|
|
|
68
68
|
rescue SyntaxTree::Parser::ParseError => error
|
|
69
69
|
raise ParseError.new(error, source: code, module_id: module_id)
|
|
70
70
|
end
|
|
71
|
-
TransformResult.new(
|
|
71
|
+
TransformResult.new(
|
|
72
|
+
result.code,
|
|
73
|
+
result.dependencies,
|
|
74
|
+
nil,
|
|
75
|
+
[],
|
|
76
|
+
result.watched_patterns,
|
|
77
|
+
{ruby_constants: top_level_constants(program)}
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The value an importer receives: the requested constant, or Default
|
|
82
|
+
# when the module defines one. Nil keeps the Exports module.
|
|
83
|
+
def import_value(resolved_dependency, record, context)
|
|
84
|
+
name = import_constant_name(resolved_dependency, record)
|
|
85
|
+
return nil unless name
|
|
86
|
+
|
|
87
|
+
context.mods.fetch(record.id).const_get(:Exports).const_get(name, false)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def runtime_import_value(resolved_dependency, record, _context)
|
|
91
|
+
name = import_constant_name(resolved_dependency, record)
|
|
92
|
+
return nil unless name
|
|
93
|
+
|
|
94
|
+
Runtime::DefaultImport.new(name)
|
|
72
95
|
end
|
|
73
96
|
|
|
74
97
|
private
|
|
75
98
|
|
|
76
99
|
# The rewriter only parses a file that contains an import it cannot
|
|
77
|
-
# rewrite literally, so without this
|
|
100
|
+
# rewrite literally, so without this parse most syntax errors would
|
|
78
101
|
# not surface until the module was evaluated -- and a production build
|
|
79
102
|
# never evaluates application modules, so the bundle would ship
|
|
80
|
-
# broken. Prism is the parser CRuby itself uses, and
|
|
81
|
-
# well under a
|
|
82
|
-
|
|
83
|
-
|
|
103
|
+
# broken. Prism is the parser CRuby itself uses, and parsing costs
|
|
104
|
+
# well under a millisecond per file. The same tree tells us which
|
|
105
|
+
# constants the module exports.
|
|
106
|
+
def parse!(module_id, code)
|
|
107
|
+
result = Prism.parse(code)
|
|
108
|
+
raise ParseError.new(result, source: code, module_id: module_id) if result.failure?
|
|
109
|
+
|
|
110
|
+
result.value
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The constants a module defines at its top level, meaning directly
|
|
114
|
+
# on its Exports module. Only static definitions count: `const_set`,
|
|
115
|
+
# constants inside `if` or `begin`, `A::B = 1`, and `class ::A` are
|
|
116
|
+
# not exports.
|
|
117
|
+
def top_level_constants(program)
|
|
118
|
+
program.statements.body.flat_map { |node| defined_constant_names(node) }.uniq
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def defined_constant_names(node)
|
|
122
|
+
case node
|
|
123
|
+
when Prism::ConstantWriteNode
|
|
124
|
+
[node.name]
|
|
125
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
126
|
+
node.constant_path.is_a?(Prism::ConstantReadNode) ? [node.constant_path.name] : []
|
|
127
|
+
when Prism::MultiWriteNode
|
|
128
|
+
[*node.lefts, *node.rights].grep(Prism::ConstantTargetNode).map(&:name)
|
|
129
|
+
else
|
|
130
|
+
[]
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Records without a constant list were not transformed by this plugin
|
|
135
|
+
# and keep whatever value other plugins or the graph supply.
|
|
136
|
+
def import_constant_name(resolved_dependency, record)
|
|
137
|
+
constants = record.metadata[:ruby_constants]
|
|
138
|
+
return nil unless constants
|
|
84
139
|
|
|
85
|
-
|
|
140
|
+
name = resolved_dependency.dependency.metadata.fetch(:import_name, :Default)
|
|
141
|
+
constants.include?(name) ? name : nil
|
|
86
142
|
end
|
|
87
143
|
end
|
|
88
144
|
end
|
|
@@ -11,7 +11,7 @@ module Klenod
|
|
|
11
11
|
module Build
|
|
12
12
|
class RubyImportRewriter
|
|
13
13
|
ImportCall =
|
|
14
|
-
Data.define(:specifier, :location, :dynamic, :method_name, :eager_override) do
|
|
14
|
+
Data.define(:specifier, :location, :dynamic, :method_name, :eager_override, :import_name) do
|
|
15
15
|
def eager
|
|
16
16
|
eager_override.nil? ? RubyImportRewriter::IMPORT_METHODS.fetch(method_name).fetch(:eager) : eager_override
|
|
17
17
|
end
|
|
@@ -213,21 +213,33 @@ module Klenod
|
|
|
213
213
|
|
|
214
214
|
first = unwrap_paren(parts.first)
|
|
215
215
|
string_parts = first&.instance_variable_get(:@parts)
|
|
216
|
-
|
|
217
|
-
(parts.length == 1) &&
|
|
216
|
+
literal_specifier =
|
|
218
217
|
first&.class&.name == "SyntaxTree::StringLiteral" &&
|
|
219
218
|
string_parts&.length == 1 &&
|
|
220
219
|
string_parts.first.instance_of?(::SyntaxTree::TStringContent)
|
|
220
|
+
import_name = (parts.length == 2) ? constant_symbol_value(parts.fetch(1)) : nil
|
|
221
|
+
literal = literal_specifier && (parts.length == 1 || !import_name.nil?)
|
|
221
222
|
|
|
222
223
|
ImportCall.new(
|
|
223
224
|
literal ? string_parts.first.value : nil,
|
|
224
225
|
node.instance_variable_get(:@location),
|
|
225
226
|
!literal,
|
|
226
227
|
node.instance_variable_get(:@message).value,
|
|
227
|
-
nil
|
|
228
|
+
nil,
|
|
229
|
+
literal ? import_name : nil
|
|
228
230
|
)
|
|
229
231
|
end
|
|
230
232
|
|
|
233
|
+
# The optional second argument of `import`: a symbol literal spelled like
|
|
234
|
+
# a constant, such as `:Bar`. Anything else (`:bar`, `:"Bar"`, a string,
|
|
235
|
+
# a variable) is reported as a dynamic import.
|
|
236
|
+
def constant_symbol_value(node)
|
|
237
|
+
return unless node.instance_of?(::SyntaxTree::SymbolLiteral)
|
|
238
|
+
|
|
239
|
+
value = node.instance_variable_get(:@value)
|
|
240
|
+
value.instance_of?(::SyntaxTree::Const) ? value.value.to_sym : nil
|
|
241
|
+
end
|
|
242
|
+
|
|
231
243
|
def build_import_glob_from_parts(node, parts)
|
|
232
244
|
first = unwrap_paren(parts.first)
|
|
233
245
|
string_parts = first&.instance_variable_get(:@parts)
|
|
@@ -250,7 +262,8 @@ module Klenod
|
|
|
250
262
|
node.instance_variable_get(:@location),
|
|
251
263
|
!literal || !valid_options,
|
|
252
264
|
node.instance_variable_get(:@message).value,
|
|
253
|
-
eager
|
|
265
|
+
eager,
|
|
266
|
+
nil
|
|
254
267
|
)
|
|
255
268
|
end
|
|
256
269
|
|
|
@@ -304,20 +317,29 @@ module Klenod
|
|
|
304
317
|
end
|
|
305
318
|
|
|
306
319
|
def raise_dynamic_import!(call)
|
|
307
|
-
expected =
|
|
320
|
+
expected =
|
|
321
|
+
if call.method_name == "import_glob"
|
|
322
|
+
"import_glob(\"...\")"
|
|
323
|
+
else
|
|
324
|
+
"#{call.method_name}(\"...\") or #{call.method_name}(\"...\", :Constant)"
|
|
325
|
+
end
|
|
308
326
|
raise DynamicImportError, "Only literal #{expected} calls are supported in #{@module_id}"
|
|
309
327
|
end
|
|
310
328
|
|
|
311
329
|
def build_dependency(call, dependency_index)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
330
|
+
dependency =
|
|
331
|
+
Dependency
|
|
332
|
+
.create(
|
|
333
|
+
specifier: call.specifier,
|
|
334
|
+
importer_id: @module_id,
|
|
335
|
+
kind: @kind,
|
|
336
|
+
loc: source_location(call.location)
|
|
337
|
+
)
|
|
338
|
+
.with(eager: call.eager)
|
|
339
|
+
.with(id: "#{@module_id}:dependency:#{dependency_index}")
|
|
340
|
+
return dependency unless call.import_name
|
|
341
|
+
|
|
342
|
+
dependency.with(metadata: {import_name: call.import_name}.freeze)
|
|
321
343
|
end
|
|
322
344
|
|
|
323
345
|
def expand_glob_import(call, dependency_index)
|
|
@@ -110,6 +110,43 @@ module Klenod
|
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
# A named import (`import("./x", :Bar)`) of a constant the target module
|
|
114
|
+
# does not define at its top level.
|
|
115
|
+
#
|
|
116
|
+
# The check uses the constants the Ruby plugin recorded statically, so a
|
|
117
|
+
# typo fails while collecting, with an excerpt of the import, instead of
|
|
118
|
+
# surfacing as a NameError from inside module evaluation -- or, worse, at
|
|
119
|
+
# runtime, since a production build never evaluates application modules.
|
|
120
|
+
class MissingExportError < SourceError
|
|
121
|
+
Report = Data.define(:dependency, :target_id, :name, :constants)
|
|
122
|
+
|
|
123
|
+
def kind
|
|
124
|
+
"Missing export"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def location(report)
|
|
130
|
+
Location.new(
|
|
131
|
+
line: report.dependency.loc&.line,
|
|
132
|
+
column: report.dependency.loc&.column,
|
|
133
|
+
detail: "#{report.target_id} does not define #{report.name}",
|
|
134
|
+
hints: [hint_for(report)]
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def hint_for(report)
|
|
139
|
+
case report.constants
|
|
140
|
+
when nil
|
|
141
|
+
"Named imports need a Ruby module; #{report.target_id} is not one"
|
|
142
|
+
when []
|
|
143
|
+
"#{report.target_id} defines no top-level constants; import it without a name to get its Exports module"
|
|
144
|
+
else
|
|
145
|
+
"It defines #{report.constants.join(", ")}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
113
150
|
# Ruby generated from another format that does not parse.
|
|
114
151
|
#
|
|
115
152
|
# This is a bug in the plugin that generated it rather than in anything the
|
data/lib/klenod/build/version.rb
CHANGED
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.18
|
|
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.18
|
|
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.18
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: async
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|