pray-cli 1.9.2 → 1.10.0

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.
data/lib/pray/render.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "fileutils"
4
4
  require "pathname"
5
+ require_relative "render_content"
5
6
 
6
7
  module Pray
7
8
  RenderedTarget = Struct.new(:path, :content, :managed_spans) do
@@ -23,33 +24,29 @@ module Pray
23
24
  rendered
24
25
  end
25
26
 
26
- def write_rendered_targets(project, rendered)
27
+ def write_rendered_targets(project, rendered, previous_lockfile = nil)
27
28
  rendered.each do |target|
28
- PathSafety.validate_project_relative_path!(target.path)
29
+ PathSafety.validate_destination_path!(target.path)
30
+ RenderDest.ensure_safe_destination_ancestors!(project.project_root, target.path, target.path)
29
31
  path = File.join(project.project_root, target.path)
30
32
  FileUtils.mkdir_p(File.dirname(path))
31
- File.write(path, target.content)
33
+ RenderDest.ensure_safe_destination_ancestors!(project.project_root, target.path, target.path)
34
+ RenderDest.write_rendered_content(path, target.path, target.content)
32
35
  end
33
- materialize_provisioned_exports(project)
36
+ materialize_provisioned_exports(project, previous_lockfile)
34
37
  end
35
38
 
36
- def materialize_provisioned_exports(project)
37
- symbols = project.manifest.symbols || {}
38
- planned_provisioned_files(project).each do |file|
39
- PathSafety.validate_project_relative_path!(file.path)
40
- destination = File.join(project.project_root, file.path)
41
- FileUtils.mkdir_p(File.dirname(destination))
42
- write_provisioned_file(file.source, destination, symbols)
43
- end
39
+ def materialize_provisioned_exports(project, previous_lockfile = nil)
40
+ RenderDest.materialize(project, previous_lockfile)
44
41
  end
45
42
 
46
- def write_provisioned_file(source, destination, symbols)
43
+ def expected_provisioned_bytes(source, symbols)
47
44
  bytes = File.binread(source)
48
- text = bytes.force_encoding(Encoding::UTF_8)
45
+ text = bytes.dup.force_encoding(Encoding::UTF_8)
49
46
  if text.valid_encoding?
50
- File.write(destination, Substitute.substitute_pray_symbols(text, symbols))
47
+ Substitute.substitute_pray_symbols(text, symbols)
51
48
  else
52
- File.binwrite(destination, bytes)
49
+ bytes
53
50
  end
54
51
  end
55
52
 
@@ -71,9 +68,10 @@ module Pray
71
68
  planned.sort_by(&:path).uniq(&:path)
72
69
  end
73
70
 
74
- PlannedProvisionedFile = Struct.new(:path, :source)
71
+ PlannedProvisionedFile = Struct.new(:path, :source, :package, :export, keyword_init: true)
75
72
 
76
73
  def render_target(project, target, output)
74
+ ComposeDest.ensure_html_comment_dest!(output)
77
75
  if target.scoped && target.mode == "compose"
78
76
  return render_scoped_compose(project, target, output)
79
77
  end
@@ -82,7 +80,7 @@ module Pray
82
80
 
83
81
  def render_scoped_compose(project, target, output)
84
82
  builder = ContentBuilder.new
85
- append_header(builder, project, output)
83
+ append_header(builder, project, target, output)
86
84
  symbols = project.manifest.symbols || {}
87
85
  managed_spans = []
88
86
 
@@ -99,7 +97,7 @@ module Pray
99
97
 
100
98
  def render_legacy_compose(project, target, output)
101
99
  builder = ContentBuilder.new
102
- append_header(builder, project, output)
100
+ append_header(builder, project, target, output)
103
101
 
104
102
  unbound_locals = project.local_files.select do |local|
105
103
  declaration = project.manifest.local.find { |entry| entry.path == local.manifest_path }
@@ -137,16 +135,11 @@ module Pray
137
135
  RenderedTarget.new(path: output, content: builder.finish, managed_spans: managed_spans)
138
136
  end
139
137
 
140
- def append_header(builder, project, output)
141
- return unless project.manifest.render.header
138
+ def append_header(builder, project, target, output)
139
+ text = ComposeDest.header_text(target, output, project.manifest.render.header)
140
+ return unless text
142
141
 
143
- output_name = File.basename(output)
144
- builder.append_line("<!-- pray:0 ignore-comments -->")
145
- builder.append_empty_line
146
- builder.append_line("# Agent context")
147
- builder.append_empty_line
148
- builder.append_line("Do not edit managed blocks in `#{output_name}` or provisioned files under `.agents/`.")
149
- builder.append_line("To change shared guidance, update `Prayfile` and run `pray install`.")
142
+ builder.append_body(text)
150
143
  builder.append_empty_line
151
144
  end
152
145
 
@@ -173,7 +166,10 @@ module Pray
173
166
 
174
167
  def append_managed_export(builder, managed_spans, package, export, target, output, symbols)
175
168
  body = package.export_bodies[export]
176
- raise Error.render("package #{package.declaration.name} is missing cached export #{export}") unless body
169
+ unless body
170
+ raise Error.integrity("compose cannot write binary export #{export}; use file: for unmarked bytes") if package.spec.exports[export]&.kind == "file"
171
+ raise Error.render("package #{package.declaration.name} is missing cached export #{export}")
172
+ end
177
173
 
178
174
  body = Substitute.substitute_pray_symbols(body, symbols)
179
175
  identifier = Hashing.marker_id("#{package.declaration.name}:#{export}:#{target.name}")
@@ -198,7 +194,7 @@ module Pray
198
194
 
199
195
  def should_inline_export?(package, export_name)
200
196
  export = package.spec.exports[export_name]
201
- export.nil? || export.kind == "fragment"
197
+ export.nil? || %w[fragment file].include?(export.kind)
202
198
  end
203
199
 
204
200
  def collect_exact_file_bindings(project, planned)
@@ -215,7 +211,12 @@ module Pray
215
211
  source = File.join(package.root, export.path)
216
212
  raise Error.render("file export source missing: #{source}") unless File.file?(source)
217
213
 
218
- planned << PlannedProvisionedFile.new(path: destination, source: source)
214
+ planned << PlannedProvisionedFile.new(
215
+ path: destination,
216
+ source: source,
217
+ package: package.declaration.name,
218
+ export: export_name
219
+ )
219
220
  matched = true
220
221
  break
221
222
  end
@@ -241,6 +242,8 @@ module Pray
241
242
  skill_files,
242
243
  [],
243
244
  [],
245
+ package.declaration.name,
246
+ skill_name,
244
247
  planned
245
248
  )
246
249
  end
@@ -274,6 +277,8 @@ module Pray
274
277
  indexed_files,
275
278
  export.only || [],
276
279
  export.except || [],
280
+ package.declaration.name,
281
+ export_name,
277
282
  planned
278
283
  )
279
284
  when "file"
@@ -285,7 +290,9 @@ module Pray
285
290
  destination = File.join(destination_root, export_name, File.basename(source))
286
291
  planned << PlannedProvisionedFile.new(
287
292
  path: relative_project_path(project, destination),
288
- source: source
293
+ source: source,
294
+ package: package.declaration.name,
295
+ export: export_name
289
296
  )
290
297
  end
291
298
  end
@@ -295,7 +302,7 @@ module Pray
295
302
  File.basename(export_path.delete_suffix("/")).empty? ? export_name : File.basename(export_path.delete_suffix("/"))
296
303
  end
297
304
 
298
- def collect_tree_files(project, source_root, destination_root, relative_files, only, except, planned)
305
+ def collect_tree_files(project, source_root, destination_root, relative_files, only, except, package_name, export_name, planned)
299
306
  raise Error.render("folder source directory missing: #{source_root}") unless File.directory?(source_root)
300
307
  raise Error.render("no files listed in package manifest for #{source_root}") if relative_files.empty?
301
308
 
@@ -310,7 +317,9 @@ module Pray
310
317
  destination = File.join(destination_root, relative)
311
318
  planned << PlannedProvisionedFile.new(
312
319
  path: relative_project_path(project, destination),
313
- source: source
320
+ source: source,
321
+ package: package_name,
322
+ export: export_name
314
323
  )
315
324
  matched = true
316
325
  end
@@ -325,36 +334,5 @@ module Pray
325
334
  rescue ArgumentError
326
335
  absolute
327
336
  end
328
-
329
- class ContentBuilder
330
- def initialize
331
- @content = +""
332
- end
333
-
334
- def next_line_number
335
- @content.count("\n") + 1
336
- end
337
-
338
- def append_line(line)
339
- @content << line << "\n"
340
- end
341
-
342
- def append_empty_line
343
- @content << "\n"
344
- end
345
-
346
- def append_body(body)
347
- trimmed = body.sub(/\n+\z/, "")
348
- return if trimmed.empty?
349
-
350
- trimmed.each_line(chomp: true) { |line| append_line(line) }
351
- end
352
-
353
- def finish
354
- @content.sub(/\n\n+\z/, "\n")
355
- @content << "\n" unless @content.end_with?("\n")
356
- @content
357
- end
358
- end
359
337
  end
360
338
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module Render
5
+ class ContentBuilder
6
+ def initialize
7
+ @content = +""
8
+ end
9
+
10
+ def next_line_number
11
+ @content.count("\n") + 1
12
+ end
13
+
14
+ def append_line(line)
15
+ @content << line << "\n"
16
+ end
17
+
18
+ def append_empty_line
19
+ @content << "\n"
20
+ end
21
+
22
+ def append_body(body)
23
+ trimmed = body.sub(/\n+\z/, "")
24
+ return if trimmed.empty?
25
+
26
+ trimmed.each_line(chomp: true) { |line| append_line(line) }
27
+ end
28
+
29
+ def finish
30
+ @content.sub(/\n\n+\z/, "\n")
31
+ @content << "\n" unless @content.end_with?("\n")
32
+ @content
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "pathname"
5
+
6
+ module Pray
7
+ module RenderDest
8
+ module_function
9
+
10
+ def materialize(project, previous_lockfile = nil)
11
+ planned = Render.planned_provisioned_files(project)
12
+ previous = previous_map(previous_lockfile)
13
+ planned_paths = planned.map(&:path).to_h { |path| [path, true] }
14
+ planned.each { |file| write_leaf(project, file, previous) }
15
+ prune_dropped(project, previous_lockfile, planned_paths) if previous_lockfile
16
+ end
17
+
18
+ def provisioned_records(project)
19
+ symbols = project.manifest.symbols || {}
20
+ Render.planned_provisioned_files(project).map do |file|
21
+ expected = Render.expected_provisioned_bytes(file.source, symbols)
22
+ ProvisionedFileRecord.new(
23
+ path: file.path.to_s.tr("\\", "/"),
24
+ content_hash: Hashing.sha256_prefixed(expected.b),
25
+ package: file.package,
26
+ export: file.export
27
+ )
28
+ end
29
+ end
30
+
31
+ def fail_if_symlink!(path, display)
32
+ return unless File.symlink?(path)
33
+
34
+ raise Error.render("refusing to write `#{display}` because it is a symbolic link")
35
+ end
36
+
37
+ def previous_map(lockfile)
38
+ return {} unless lockfile
39
+
40
+ Array(lockfile.provisioned).to_h { |record| [record.path, record] }
41
+ end
42
+
43
+ def destination_status(project, file, previous_lockfile = nil)
44
+ PathSafety.validate_destination_path!(file.path)
45
+ ensure_safe_destination_ancestors!(project.project_root, file.path, file.path)
46
+ destination = File.join(project.project_root, file.path)
47
+ expected = Render.expected_provisioned_bytes(file.source, project.manifest.symbols || {})
48
+ record = previous_map(previous_lockfile)[file.path.to_s.tr("\\", "/")]
49
+ classify_destination(destination, file.path, expected, record)
50
+ end
51
+
52
+ def write_leaf(project, file, previous)
53
+ PathSafety.validate_destination_path!(file.path)
54
+ ensure_safe_destination_ancestors!(project.project_root, file.path, file.path)
55
+ destination = File.join(project.project_root, file.path)
56
+ expected = Render.expected_provisioned_bytes(file.source, project.manifest.symbols || {})
57
+ record = previous[file.path.to_s.tr("\\", "/")]
58
+ status = classify_destination(destination, file.path, expected, record)
59
+ if status == :write
60
+ FileUtils.mkdir_p(File.dirname(destination))
61
+ ensure_safe_destination_ancestors!(project.project_root, file.path, file.path)
62
+ return create_bytes(destination, file.path, expected)
63
+ end
64
+ return if status == :unchanged
65
+
66
+ unless record
67
+ raise Error.render("missing lock ownership for `#{file.path}`")
68
+ end
69
+
70
+ ensure_safe_destination_ancestors!(project.project_root, file.path, file.path)
71
+ update_bytes(destination, file.path, expected, record.content_hash)
72
+ end
73
+
74
+ def classify_destination(destination, display, expected, record)
75
+ kind = destination_kind(destination)
76
+ return :write if kind == :missing
77
+
78
+ fail_if_symlink!(destination, display)
79
+ unless kind == :regular
80
+ raise Error.render("refusing to write `#{display}`; destination is not a regular file")
81
+ end
82
+ on_disk = read_regular_bytes(destination, display)
83
+ return :unchanged if on_disk.b == expected.b
84
+ return :update if record && Hashing.sha256_prefixed(on_disk) == record.content_hash
85
+
86
+ if record
87
+ raise Error.render("refusing to overwrite `#{display}`; it was provisioned and then edited")
88
+ end
89
+
90
+ raise Error.render(
91
+ "refusing to overwrite `#{display}`; it already exists and is not the expected provisioned file"
92
+ )
93
+ end
94
+
95
+ def prune_dropped(project, previous, planned_paths)
96
+ Array(previous.provisioned).each do |record|
97
+ PathSafety.validate_destination_path!(record.path)
98
+ next if planned_paths[record.path]
99
+
100
+ destination = File.join(project.project_root, record.path)
101
+ ensure_safe_destination_ancestors!(project.project_root, record.path, record.path)
102
+ next unless destination_kind(destination) == :regular
103
+
104
+ on_disk = read_regular_bytes(destination, record.path)
105
+ if Hashing.sha256_prefixed(on_disk) == record.content_hash
106
+ ensure_safe_destination_ancestors!(project.project_root, record.path, record.path)
107
+ File.delete(destination)
108
+ end
109
+ end
110
+ end
111
+
112
+ def layout_rendered_content(path, display, fresh)
113
+ kind = destination_kind(path)
114
+ return fresh if kind == :missing
115
+
116
+ fail_if_symlink!(path, display)
117
+ unless kind == :regular
118
+ raise Error.render("refusing to write `#{display}`; destination is not a regular file")
119
+ end
120
+ RenderPatch.patch_rendered_content(decode_utf8(read_regular_bytes(path, display), display), fresh)
121
+ end
122
+
123
+ def write_rendered_content(path, display, fresh)
124
+ return create_bytes(path, display, fresh) if destination_kind(path) == :missing
125
+
126
+ open_regular(path, display, File::RDWR) do |file|
127
+ existing = decode_utf8(file.read, display)
128
+ content = RenderPatch.patch_rendered_content(existing, fresh)
129
+ file.rewind
130
+ file.truncate(0)
131
+ file.write(content)
132
+ end
133
+ end
134
+
135
+ def ensure_safe_destination_ancestors!(project_root, relative_path, display)
136
+ parent = File.dirname(relative_path.to_s.tr("\\", "/"))
137
+ return if parent == "."
138
+
139
+ current = project_root
140
+ parent.split("/").each do |component|
141
+ next if component.empty? || component == "."
142
+
143
+ current = File.join(current, component)
144
+ metadata = File.lstat(current)
145
+ if metadata.symlink?
146
+ raise Error.render(
147
+ "refusing to write `#{display}` because a destination parent is a symbolic link"
148
+ )
149
+ end
150
+ unless metadata.directory?
151
+ raise Error.render(
152
+ "refusing to write `#{display}`; a destination parent is not a directory"
153
+ )
154
+ end
155
+ rescue Errno::ENOENT
156
+ next
157
+ end
158
+ end
159
+
160
+ def create_bytes(path, display, bytes)
161
+ open_path(path, display, File::WRONLY | File::CREAT | File::EXCL) do |file|
162
+ file.write(bytes)
163
+ end
164
+ end
165
+
166
+ def update_bytes(path, display, bytes, authorized_hash)
167
+ open_regular(path, display, File::RDWR) do |file|
168
+ on_disk = file.read
169
+ return if on_disk.b == bytes.b
170
+
171
+ unless Hashing.sha256_prefixed(on_disk) == authorized_hash
172
+ raise Error.render("refusing to overwrite `#{display}`; it was provisioned and then edited")
173
+ end
174
+ file.rewind
175
+ file.truncate(0)
176
+ file.write(bytes)
177
+ end
178
+ end
179
+
180
+ def read_regular_bytes(path, display)
181
+ open_regular(path, display, File::RDONLY) { |file| file.read }
182
+ end
183
+
184
+ def decode_utf8(bytes, display)
185
+ text = bytes.dup.force_encoding(Encoding::UTF_8)
186
+ return text if text.valid_encoding?
187
+
188
+ raise Error.render("rendered destination `#{display}` is not valid UTF-8")
189
+ end
190
+
191
+ def open_regular(path, display, flags)
192
+ open_path(path, display, flags) do |file|
193
+ unless file.stat.file?
194
+ raise Error.render("refusing to write `#{display}`; destination is not a regular file")
195
+ end
196
+ yield file
197
+ end
198
+ end
199
+
200
+ def open_path(path, display, flags)
201
+ no_follow = File.const_defined?(:NOFOLLOW) ? File::NOFOLLOW : 0
202
+ File.open(path, flags | no_follow) { |file| yield file }
203
+ rescue Errno::ELOOP
204
+ raise Error.render("refusing to write `#{display}` because it is a symbolic link")
205
+ end
206
+
207
+ def destination_kind(path)
208
+ return :symlink if File.symlink?(path)
209
+ return :missing unless File.exist?(path)
210
+ return :regular if File.file?(path)
211
+
212
+ :other
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module Render
5
+ module_function
6
+
7
+ def layout_rendered_targets(project, rendered)
8
+ rendered.map do |target|
9
+ PathSafety.validate_destination_path!(target.path)
10
+ RenderDest.ensure_safe_destination_ancestors!(project.project_root, target.path, target.path)
11
+ path = File.join(project.project_root, target.path)
12
+ content = RenderDest.layout_rendered_content(path, target.path, target.content)
13
+ RenderedTarget.new(
14
+ path: target.path,
15
+ content: content,
16
+ managed_spans: RenderPatch.relocate_managed_spans(content, target.managed_spans)
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pray
4
+ module RenderPatch
5
+ module_function
6
+
7
+ def patch_rendered_content(existing, fresh)
8
+ existing_segments = split_segments(existing)
9
+ fresh_segments = split_segments(fresh)
10
+ fresh_managed = fresh_segments.filter_map do |segment|
11
+ [segment[:id], segment[:body]] if segment[:kind] == :managed
12
+ end.to_h
13
+ overlap = existing_segments.any? do |segment|
14
+ segment[:kind] == :managed && fresh_managed.key?(segment[:id])
15
+ end
16
+ return fresh unless overlap
17
+
18
+ used = {}
19
+ output = existing_segments.map do |segment|
20
+ next segment[:text] if segment[:kind] == :text
21
+
22
+ used[segment[:id]] = true
23
+ managed_segment(segment[:id], fresh_managed.fetch(segment[:id], segment[:body]))
24
+ end.join
25
+ fresh_segments.each do |segment|
26
+ next unless segment[:kind] == :managed
27
+ next if used[segment[:id]]
28
+
29
+ output << managed_segment(segment[:id], segment[:body])
30
+ end
31
+ output.end_with?("\n") ? output : "#{output}\n"
32
+ end
33
+
34
+ def relocate_managed_spans(content, spans)
35
+ positions = marker_positions(lines_of(content))
36
+ spans.map do |span|
37
+ position = positions[span.id]
38
+ next span unless position
39
+
40
+ span.dup.tap do |relocated|
41
+ relocated.open_line = position[0]
42
+ relocated.close_line = position[1]
43
+ end
44
+ end
45
+ end
46
+
47
+ def split_segments(content)
48
+ lines = lines_of(content)
49
+ segments = []
50
+ text = +""
51
+ index = 0
52
+ while index < lines.length
53
+ identifier = marker_id(lines[index])
54
+ close = find_closing_marker(lines, index + 1, identifier) if identifier
55
+ if identifier && close
56
+ segments << {kind: :text, text: text} unless text.empty?
57
+ text = +""
58
+ body_lines = lines[(index + 1)...close]
59
+ body = body_lines.empty? ? "" : "#{body_lines.join("\n")}\n"
60
+ segments << {kind: :managed, id: identifier, body: body}
61
+ index = close + 1
62
+ else
63
+ text << "#{lines[index]}\n"
64
+ index += 1
65
+ end
66
+ end
67
+ segments << {kind: :text, text: text} unless text.empty?
68
+ segments
69
+ end
70
+
71
+ def lines_of(content)
72
+ content.lines(chomp: true)
73
+ end
74
+
75
+ def find_closing_marker(lines, start, identifier)
76
+ (start...lines.length).find { |index| marker_id(lines[index]) == identifier }
77
+ end
78
+
79
+ def marker_positions(lines)
80
+ positions = {}
81
+ active = nil
82
+ lines.each_with_index do |line, index|
83
+ identifier = marker_id(line)
84
+ next unless identifier&.match?(/\A[a-z0-9]+\z/)
85
+
86
+ if active.nil?
87
+ active = [identifier, index + 1]
88
+ elsif active[0] == identifier
89
+ positions[identifier] = [active[1], index + 1]
90
+ active = nil
91
+ end
92
+ end
93
+ positions
94
+ end
95
+
96
+ def marker_id(line)
97
+ match = line.strip.match(/\A<!-- pray:(.+) -->\z/)
98
+ identifier = match && match[1]
99
+ identifier unless identifier == "0 ignore-comments"
100
+ end
101
+
102
+ def managed_segment(identifier, body)
103
+ content = body.empty? ? "" : "#{body.sub(/\n+\z/, "")}\n"
104
+ "<!-- pray:#{identifier} -->\n#{content}<!-- pray:#{identifier} -->\n"
105
+ end
106
+ end
107
+ end
data/lib/pray/resolve.rb CHANGED
@@ -87,6 +87,7 @@ module Pray
87
87
  errors << "#{declaration.name}: #{error.message}"
88
88
  end
89
89
  raise Error.resolution(errors.join("\n")) unless errors.empty?
90
+ PackageSpec.warn_resolved_deprecations(packages)
90
91
 
91
92
  local_files = []
92
93
  local_errors = []
@@ -298,40 +299,11 @@ module Pray
298
299
  end
299
300
 
300
301
  def select_exports(declaration, spec)
301
- unless declaration.exports.empty?
302
- declaration.exports.each do |export|
303
- unless spec.exports.key?(export)
304
- raise Error.resolution("package #{declaration.name} does not export #{export}")
305
- end
306
- end
307
- return declaration.exports
308
- end
309
-
310
- roles = declaration.roles || []
311
- return spec.exports.keys.sort if roles.empty? && declaration.file.nil?
312
-
313
- effective_roles = roles.dup
314
- effective_roles << "file" if declaration.file && !effective_roles.include?("file")
302
+ ResolveExports.select_exports(declaration, spec)
303
+ end
315
304
 
316
- selected = []
317
- effective_roles.each do |role|
318
- compatible = spec.exports.filter_map do |name, export|
319
- name if Destination.export_kind_matches_role?(export.kind, role)
320
- end
321
- case compatible.length
322
- when 1
323
- selected << compatible.first unless selected.include?(compatible.first)
324
- when 0
325
- raise Error.resolution(
326
- "package #{declaration.name} has no export compatible with #{role}"
327
- )
328
- else
329
- raise Error.resolution(
330
- "package #{declaration.name} has multiple exports compatible with #{role}; set export: \"name\""
331
- )
332
- end
333
- end
334
- selected
305
+ def load_export_bodies(file_bytes, spec, selected_exports)
306
+ ResolveExports.load_export_bodies(file_bytes, spec, selected_exports)
335
307
  end
336
308
 
337
309
  def load_package_file_bytes(root, spec)
@@ -346,21 +318,6 @@ module Pray
346
318
  file_bytes
347
319
  end
348
320
 
349
- def load_export_bodies(file_bytes, spec, selected_exports)
350
- export_bodies = {}
351
- selected_exports.each do |export_name|
352
- entry = spec.exports[export_name]
353
- raise Error.resolution("package #{spec.name} is missing export #{export_name}") unless entry
354
- next unless entry.kind == "fragment"
355
-
356
- bytes = file_bytes[entry.path]
357
- raise Error.integrity("package file missing for export #{export_name}: #{entry.path}") unless bytes
358
-
359
- export_bodies[export_name] = Hashing.normalize_line_endings(bytes.force_encoding(Encoding::UTF_8))
360
- end
361
- export_bodies
362
- end
363
-
364
321
  def build_skill_file_index(spec)
365
322
  index = {}
366
323
  spec.exports.each do |export_name, export|