proscenium 0.26.1-aarch64-linux-gnu → 0.27.0-aarch64-linux-gnu

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +50 -3
  3. data/lib/proscenium/app_package.rb +8 -4
  4. data/lib/proscenium/builder/request.rb +72 -0
  5. data/lib/proscenium/builder.rb +10 -74
  6. data/lib/proscenium/cli/bun_linker.rb +2 -2
  7. data/lib/proscenium/cli/collisions.rb +7 -9
  8. data/lib/proscenium/cli/error.rb +58 -265
  9. data/lib/proscenium/cli/gem_archive.rb +4 -2
  10. data/lib/proscenium/cli/gem_check.rb +13 -11
  11. data/lib/proscenium/cli/gitignore.rb +2 -1
  12. data/lib/proscenium/cli/inspect.rb +12 -11
  13. data/lib/proscenium/cli/install.rb +35 -46
  14. data/lib/proscenium/cli/manager.rb +6 -6
  15. data/lib/proscenium/cli/platform_variants.rb +8 -8
  16. data/lib/proscenium/cli/pnpm_approvals.rb +3 -3
  17. data/lib/proscenium/cli/pnpm_workspace.rb +15 -14
  18. data/lib/proscenium/cli/project_lock.rb +3 -3
  19. data/lib/proscenium/cli/registration.rb +5 -5
  20. data/lib/proscenium/cli/reporter.rb +2 -2
  21. data/lib/proscenium/cli/verify.rb +9 -50
  22. data/lib/proscenium/cli.rb +5 -4
  23. data/lib/proscenium/contexts/map.rb +183 -0
  24. data/lib/proscenium/contexts/mapping_generation.rb +68 -0
  25. data/lib/proscenium/contexts/messages.rb +276 -0
  26. data/lib/proscenium/{cli/contexts.rb → contexts/plan.rb} +12 -12
  27. data/lib/proscenium/contexts/projection.rb +77 -0
  28. data/lib/proscenium/{cli → contexts}/rules.rb +1 -1
  29. data/lib/proscenium/contexts/stale.rb +174 -0
  30. data/lib/proscenium/contexts.rb +16 -0
  31. data/lib/proscenium/css_module/path.rb +1 -0
  32. data/lib/proscenium/css_module/transformer.rb +15 -0
  33. data/lib/proscenium/css_module.rb +3 -2
  34. data/lib/proscenium/ext/proscenium +0 -0
  35. data/lib/proscenium/helper.rb +6 -1
  36. data/lib/proscenium/importer.rb +13 -1
  37. data/lib/proscenium/middleware/base.rb +0 -4
  38. data/lib/proscenium/middleware.rb +0 -2
  39. data/lib/proscenium/railtie.rb +4 -4
  40. data/lib/proscenium/resolver.rb +3 -1
  41. data/lib/proscenium/side_load.rb +6 -2
  42. data/lib/proscenium/source_path.rb +2 -0
  43. data/lib/proscenium/utils.rb +2 -1
  44. data/lib/proscenium/version.rb +1 -1
  45. data/lib/proscenium.rb +5 -5
  46. metadata +10 -7
  47. data/lib/proscenium/context_map.rb +0 -181
  48. data/lib/proscenium/dependency_context.rb +0 -74
  49. data/lib/proscenium/mapping_generation.rb +0 -64
  50. data/lib/proscenium/stale_contexts.rb +0 -118
@@ -3,10 +3,10 @@
3
3
  require 'fileutils'
4
4
  require 'bundler'
5
5
  require_relative '../bundled_gems'
6
- require_relative '../dependency_context'
7
- require_relative '../stale_contexts'
6
+ require_relative '../contexts/projection'
7
+ require_relative '../contexts/stale'
8
8
  require_relative 'collisions'
9
- require_relative 'contexts'
9
+ require_relative '../contexts/plan'
10
10
  require_relative 'diff'
11
11
  require_relative 'manager'
12
12
  require_relative 'platform_variants'
@@ -54,10 +54,11 @@ module Proscenium
54
54
 
55
55
  contexts.flat_map do |gem, json|
56
56
  context = JSON.parse(json)
57
- Rules::DEPENDENCY_FIELDS.flat_map { (context[it] || {}).keys }
58
- .reject { it.start_with?('@rubygems/') }
59
- .select { output.match?(named(it)) }
60
- .map { [gem, it] }
57
+ fields = Contexts::Rules::DEPENDENCY_FIELDS
58
+ fields.flat_map { (context[it] || {}).keys }
59
+ .reject { it.start_with?('@rubygems/') }
60
+ .select { output.match?(named(it)) }
61
+ .map { [gem, it] }
61
62
  end
62
63
  end
63
64
 
@@ -86,9 +87,9 @@ module Proscenium
86
87
  specs = timed(:bundle) { BundledGems.installed_specs }
87
88
  @excluded = BundledGems.excluded_names(specs)
88
89
  @contexts = timed(:projection) do
89
- Contexts.new(@root, BundledGems.participating(specs, overrides:))
90
+ Contexts::Plan.new(@root, BundledGems.participating(specs, overrides:))
90
91
  end
91
- @committed = Contexts.committed(@root)
92
+ @committed = Contexts::Plan.committed(@root)
92
93
  check_owned_directory
93
94
  report_problems
94
95
  check_platform_variants(specs, overrides)
@@ -107,7 +108,7 @@ module Proscenium
107
108
  @contexts.problems.each do |code, args|
108
109
  @reporter.error(Error.new(code, **args), phase: 'validate', manager: @manager.name)
109
110
  end
110
- raise Error.new('PSM-E-PROBLEMS', count: @contexts.problems.size)
111
+ raise Error, 'PSM-E-PROBLEMS'
111
112
  end
112
113
 
113
114
  # `.proscenium/packages/` is Proscenium's: every entry is a generated context. A package.json
@@ -121,7 +122,7 @@ module Proscenium
121
122
  # macOS's .DS_Store is ours to remove; anything else, a .git included, is the user's.
122
123
  next (Dir.children(File.dirname(path)) - OURS).any? unless File.exist?(path)
123
124
 
124
- !generated?(gem, ContextMap.read_json(path))
125
+ !generated?(gem, Contexts::Map.read_json(path))
125
126
  rescue JSON::ParserError
126
127
  true
127
128
  end
@@ -135,7 +136,7 @@ module Proscenium
135
136
  # `proscenium` block naming a projection. A `proscenium` key alone is not enough, or install
136
137
  # would remove someone else's package as an orphan.
137
138
  def generated?(gem, package)
138
- package.is_a?(Hash) && package['name'] == DependencyContext.name_for(gem) &&
139
+ package.is_a?(Hash) && package['name'] == Contexts::Projection.name_for(gem) &&
139
140
  package.dig('proscenium', 'projection').is_a?(String)
140
141
  rescue TypeError
141
142
  false
@@ -143,7 +144,7 @@ module Proscenium
143
144
 
144
145
  # A file, or a link leading nowhere, where only context directories belong.
145
146
  def strays
146
- dir = File.join(@root, Contexts::DIR)
147
+ dir = File.join(@root, Contexts::Map::CONTEXTS)
147
148
  return [] unless Dir.exist?(dir)
148
149
 
149
150
  Dir.children(dir).reject { File.directory?(File.join(dir, it)) } - OURS
@@ -152,8 +153,8 @@ module Proscenium
152
153
  # Everything install writes is under .proscenium/ (C29). A link anywhere on the way, or in
153
154
  # place of a context, would send those writes outside the app.
154
155
  def check_owned_links
155
- owned = ['.proscenium', Contexts::DIR] + @committed.flat_map do |gem, path|
156
- [File.join(Contexts::DIR, gem), relative(path)]
156
+ owned = ['.proscenium', Contexts::Map::CONTEXTS] + @committed.flat_map do |gem, path|
157
+ [File.join(Contexts::Map::CONTEXTS, gem), relative(path)]
157
158
  end
158
159
  links = owned.select { File.symlink?(File.join(@root, it)) }
159
160
  raise Error.new('PSM-E-OWNED-LINK', paths: links.join(', ')) if links.any?
@@ -170,9 +171,9 @@ module Proscenium
170
171
  def check_trusted_sources
171
172
  manifests = @contexts.contexts.transform_values { JSON.parse(it.json) }
172
173
  # Bun's trust is read only when a gem reaches some package another way.
173
- return if Rules.substitutions(manifests).empty?
174
+ return if Contexts::Rules.substitutions(manifests).empty?
174
175
 
175
- found = Rules.trusted_substitutions(@manager.trusted_names, manifests)
176
+ found = Contexts::Rules.trusted_substitutions(@manager.trusted_names, manifests)
176
177
  return if found.empty?
177
178
 
178
179
  raise Error.new('PSM-E-TRUSTED-SOURCE', list: found.map { " - #{it}" }.join("\n"))
@@ -182,8 +183,7 @@ module Proscenium
182
183
  found = Collisions.find(@root, @manager.name, @contexts.contexts.keys)
183
184
  return if found.empty?
184
185
 
185
- raise Error.new('PSM-E-COLLISION', count: found.size,
186
- list: found.map { " - #{it}" }.join("\n"))
186
+ raise Error.new('PSM-E-COLLISION', list: found.map { " - #{it}" }.join("\n"))
187
187
  end
188
188
 
189
189
  # Committed contexts no participating gem owns. A locked gem that is not installed (another
@@ -213,17 +213,17 @@ module Proscenium
213
213
  end
214
214
 
215
215
  def check_drift
216
- @committed = Contexts.committed(@root)
216
+ @committed = Contexts::Plan.committed(@root)
217
217
  drift = []
218
218
  registered = Registration.registered?(@root, @manager.name)
219
219
  drift << 'the gem contexts are not registered' unless registered
220
220
  drift << "#{lockfile} is missing" unless File.exist?(File.join(@root, lockfile))
221
221
  changed.each { drift << describe_change(it) }
222
- orphans.each { drift << format(StaleContexts::ORPHAN, gem: it) }
222
+ orphans.each { drift << format(Contexts::Stale::ORPHAN, gem: it) }
223
223
  return if drift.empty?
224
224
 
225
225
  list = drift.map { " - #{it}" }.join("\n")
226
- raise Error.new('PSM-E-DRIFT', count: drift.size, list:)
226
+ raise Error.new('PSM-E-DRIFT', list:)
227
227
  end
228
228
 
229
229
  # A line for each committed context kept for a gem that is not installed.
@@ -233,26 +233,15 @@ module Proscenium
233
233
  end
234
234
  end
235
235
 
236
+ # Why a context whose bytes differ from what install writes is drift, with the diff. Frozen
237
+ # compares bytes, so one the engine still builds with, only reformatted, is a hand edit.
236
238
  def describe_change(context)
237
239
  path = @committed[context.gem]
238
- unless path && File.exist?(path)
239
- return "#{context.gem}: its dependency context hasn't been written yet"
240
- end
240
+ drift = Contexts::Stale.drift(context, path) || :edited
241
+ cause = "#{context.gem}: #{Contexts::Stale::REASONS[drift]}"
242
+ return cause if drift == :missing
241
243
 
242
- before = File.read(path)
243
- diff = Diff.unified(relative(path), before, context.json)
244
- committed = JSON.parse(before)['proscenium'] || {}
245
- current = JSON.parse(context.json)['proscenium']
246
- cause = if committed['projection'] != current['projection']
247
- 'its dependency context was written by a different version of Proscenium'
248
- elsif committed['projectionSha256'] == current['projectionSha256']
249
- 'its dependency context was edited by hand'
250
- else
251
- 'its NPM dependencies changed since its dependency context was written'
252
- end
253
- "#{context.gem}: #{cause}\n#{diff}"
254
- rescue JSON::ParserError
255
- "#{context.gem}: its dependency context is not valid JSON"
244
+ "#{cause}\n#{Diff.unified(relative(path), File.read(path), context.json)}"
256
245
  end
257
246
 
258
247
  def install
@@ -291,7 +280,7 @@ module Proscenium
291
280
  # links survive.
292
281
  def write_contexts
293
282
  changed.map do |context|
294
- dir = File.join(@root, Contexts::DIR, context.gem)
283
+ dir = File.join(@root, Contexts::Map::CONTEXTS, context.gem)
295
284
  FileUtils.mkdir_p(dir)
296
285
  path = File.join(dir, 'package.json')
297
286
  ProjectLock.write("#{path}.tmp", context.json)
@@ -302,18 +291,18 @@ module Proscenium
302
291
 
303
292
  def remove_orphans
304
293
  orphans.map do |gem|
305
- FileUtils.rm_rf(File.join(@root, Contexts::DIR, gem))
306
- "#{Contexts::DIR}/#{gem} (removed)"
294
+ FileUtils.rm_rf(File.join(@root, Contexts::Map::CONTEXTS, gem))
295
+ "#{Contexts::Map::CONTEXTS}/#{gem} (removed)"
307
296
  end
308
297
  end
309
298
 
310
- def run_manager(lock = nil)
299
+ def run_manager(lock)
311
300
  unless @reporter.json?
312
301
  @reporter.info(@reporter.paint("Running #{@manager.name} #{manager_args.join(' ')}...",
313
302
  :dim))
314
303
  end
315
304
  Runner.run(@manager.executable, manager_args, root: @root,
316
- on_spawn: lock&.method(:manager_started),
305
+ on_spawn: lock.method(:manager_started),
317
306
  manager: @manager.name)
318
307
  rescue Error => e
319
308
  raise unless e.code == 'PSM-E-NATIVE'
@@ -327,7 +316,7 @@ module Proscenium
327
316
  escape: "It involves #{involved}.")
328
317
  raise error
329
318
  ensure
330
- lock&.manager_finished
319
+ lock.manager_finished
331
320
  end
332
321
 
333
322
  def manager_args
@@ -339,7 +328,7 @@ module Proscenium
339
328
  # dependencies left out of a production install.
340
329
  if @options[:production]
341
330
  (@excluded & @committed.keys).each do |gem|
342
- args << "--filter=!#{DependencyContext.name_for(gem)}"
331
+ args << "--filter=!#{Contexts::Projection.name_for(gem)}"
343
332
  end
344
333
  end
345
334
  args + Array(@options[:js_args])
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
- require_relative '../context_map'
4
+ require_relative '../contexts/map'
5
5
  require_relative '../yaml_tree'
6
6
  require_relative 'pnpm_approvals'
7
7
  require 'open3'
@@ -53,7 +53,7 @@ module Proscenium
53
53
  path = File.join(root, 'package.json')
54
54
  return nil unless File.exist?(path)
55
55
 
56
- field = ContextMap.read_json(path)['packageManager']
56
+ field = Contexts::Map.read_json(path)['packageManager']
57
57
  field.is_a?(String) ? field.split('@').first : nil
58
58
  rescue JSON::ParserError
59
59
  nil
@@ -129,7 +129,7 @@ module Proscenium
129
129
  # probing would change the project before install could refuse anything.
130
130
  def pinned_version
131
131
  path = File.join(@root, 'package.json')
132
- field = File.exist?(path) && ContextMap.read_json(path)['packageManager']
132
+ field = File.exist?(path) && Contexts::Map.read_json(path)['packageManager']
133
133
  return unless field.is_a?(String)
134
134
 
135
135
  name, version = field.split('+').first.split('@', 2)
@@ -151,7 +151,7 @@ module Proscenium
151
151
  path = File.join(@root, 'pnpm-workspace.yaml')
152
152
  return unless File.exist?(path)
153
153
 
154
- text = ContextMap.utf8(File.read(path)).delete_prefix(ContextMap::BOM)
154
+ text = Contexts::Map.utf8(File.read(path)).delete_prefix(Contexts::Map::BOM)
155
155
  value, = YamlTree.value(text, key)
156
156
  value.value if value.is_a?(Psych::Nodes::Scalar)
157
157
  rescue Psych::SyntaxError
@@ -190,7 +190,7 @@ module Proscenium
190
190
  # registration, so a file that is not strict JSON is refused here, before any write.
191
191
  def package_json
192
192
  path = File.join(@root, 'package.json')
193
- File.exist?(path) ? ContextMap.read_json(path) : nil
193
+ File.exist?(path) ? Contexts::Map.read_json(path) : nil
194
194
  rescue JSON::ParserError
195
195
  raise Error.new('PSM-E-REGISTRATION', file: 'package.json')
196
196
  end
@@ -280,7 +280,7 @@ module Proscenium
280
280
  # here, so at worst an app that is not a member is refused.
281
281
  def member?(dir, root)
282
282
  path = File.join(dir, 'package.json')
283
- package = File.exist?(path) && ContextMap.read_json(path)
283
+ package = File.exist?(path) && Contexts::Map.read_json(path)
284
284
  return false unless package.is_a?(Hash)
285
285
 
286
286
  workspaces = package['workspaces']
@@ -2,10 +2,10 @@
2
2
 
3
3
  require 'json'
4
4
  require_relative '../bundled_gems'
5
- require_relative '../context_map'
6
- require_relative '../dependency_context'
5
+ require_relative '../contexts/map'
6
+ require_relative '../contexts/projection'
7
7
  require_relative 'gem_archive'
8
- require_relative 'rules'
8
+ require_relative '../contexts/rules'
9
9
 
10
10
  module Proscenium
11
11
  module CLI
@@ -50,14 +50,14 @@ module Proscenium
50
50
  # Whether the author contract refuses the variant, as it would on the host that installs it:
51
51
  # the projection drops scripts and workspaces, so equal projections do not show it.
52
52
  def refused?(gem, variant, text)
53
- manifest = ContextMap.parse_json(text)
53
+ manifest = Contexts::Map.parse_json(text)
54
54
  return true unless manifest.is_a?(Hash)
55
55
 
56
56
  # A host that installs it refuses a frontend root outside the gem.
57
57
  root = BundledGems.frontend_root(variant) or return true
58
58
  gyp = root.empty? ? 'binding.gyp' : File.join(root, 'binding.gyp')
59
- Rules.check(gem, manifest, binding_gyp: variant.files.include?(gyp))
60
- .any? { !Rules::WARNINGS.include?(it.first) }
59
+ Contexts::Rules.check(gem, manifest, binding_gyp: variant.files.include?(gyp))
60
+ .any? { !Contexts::Rules::WARNINGS.include?(it.first) }
61
61
  rescue JSON::ParserError
62
62
  true
63
63
  end
@@ -70,10 +70,10 @@ module Proscenium
70
70
 
71
71
  # The projection hash of a manifest's text, or nil for none that can be read.
72
72
  def projection(gem, text)
73
- manifest = text && ContextMap.parse_json(text)
73
+ manifest = text && Contexts::Map.parse_json(text)
74
74
  return nil unless manifest.is_a?(Hash)
75
75
 
76
- DependencyContext.project(gem, manifest).dig('proscenium', 'projectionSha256')
76
+ Contexts::Projection.project(gem, manifest).dig('proscenium', 'projectionSha256')
77
77
  rescue JSON::ParserError
78
78
  nil
79
79
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../context_map'
3
+ require_relative '../contexts/map'
4
4
  require_relative '../yaml_tree'
5
5
 
6
6
  module Proscenium
@@ -47,7 +47,7 @@ module Proscenium
47
47
  def setting?(path, key)
48
48
  return false unless File.exist?(path)
49
49
 
50
- text = ContextMap.utf8(File.read(path)).delete_prefix(ContextMap::BOM)
50
+ text = Contexts::Map.utf8(File.read(path)).delete_prefix(Contexts::Map::BOM)
51
51
  value, anchors = YamlTree.value(text, key)
52
52
  true?(YamlTree.node(value, anchors))
53
53
  rescue Psych::SyntaxError
@@ -58,7 +58,7 @@ module Proscenium
58
58
  path = File.join(root, 'pnpm-workspace.yaml')
59
59
  return [] unless File.exist?(path)
60
60
 
61
- text = ContextMap.utf8(File.read(path)).delete_prefix(ContextMap::BOM)
61
+ text = Contexts::Map.utf8(File.read(path)).delete_prefix(Contexts::Map::BOM)
62
62
  allowed, anchors = YamlTree.value(text, 'allowBuilds')
63
63
  listed, = YamlTree.value(text, 'onlyBuiltDependencies')
64
64
  node = ->(given) { YamlTree.node(given, anchors) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
- require_relative '../context_map'
4
+ require_relative '../contexts/map'
5
5
  require_relative 'brackets'
6
6
 
7
7
  module Proscenium
@@ -22,14 +22,14 @@ module Proscenium
22
22
  # whatever indentation its items use. The result is parsed back; anything but the old list
23
23
  # plus the pattern, with every other key unchanged, is refused rather than written.
24
24
  def splice(text)
25
- return text if ContextMap.registered_in?('pnpm-workspace.yaml', text, strict: true)
25
+ return text if Contexts::Map.registered_in?('pnpm-workspace.yaml', text, strict: true)
26
26
 
27
- text = ContextMap.utf8(text)
28
- bom = text.start_with?(ContextMap::BOM) ? ContextMap::BOM : ''
27
+ text = Contexts::Map.utf8(text)
28
+ bom = text.start_with?(Contexts::Map::BOM) ? Contexts::Map::BOM : ''
29
29
  body = text.delete_prefix(bom)
30
30
  spliced = splice_body(body)
31
31
  verify(body, spliced)
32
- if ContextMap.excludes?(ContextMap.yaml_packages(spliced))
32
+ if Contexts::Map.excludes?(Contexts::Map.yaml_packages(spliced))
33
33
  raise Error.new('PSM-E-REGISTRATION-EXCLUDED', file: 'pnpm-workspace.yaml')
34
34
  end
35
35
 
@@ -51,12 +51,12 @@ module Proscenium
51
51
  insert_into_flow(text, flow.end(0), text[flow.end(0)...close], eol)
52
52
  elsif (key = text.match(BLOCK, at))
53
53
  indent = text[key.end(0)..][/\A(?:[ \t]*(?:#.*)?\r?\n)*([ \t]*)- /, 1] || ' '
54
- text.dup.insert(key.end(0), "#{indent}- #{ContextMap::REGISTRATION}#{eol}")
54
+ text.dup.insert(key.end(0), "#{indent}- #{Contexts::Map::REGISTRATION}#{eol}")
55
55
  else
56
56
  # A new key would override a list inherited through a merge key (`<<: *defaults`).
57
57
  raise refused if merges?(text)
58
58
 
59
- "packages:#{eol} - #{ContextMap::REGISTRATION}#{eol}#{eol unless text.empty?}#{text}"
59
+ "packages:#{eol} - #{Contexts::Map::REGISTRATION}#{eol}#{eol unless text.empty?}#{text}"
60
60
  end
61
61
  end
62
62
 
@@ -69,7 +69,7 @@ module Proscenium
69
69
  end
70
70
 
71
71
  # The root mapping's `packages` key and its value, at any indentation, or nil. The last pair,
72
- # as pnpm and ContextMap read it; another mapping's `packages` key is not it.
72
+ # as pnpm and Contexts::Map read it; another mapping's `packages` key is not it.
73
73
  def root_pair(text)
74
74
  root = Psych.parse_stream(text).children.first&.root
75
75
  return unless root.is_a?(Psych::Nodes::Mapping)
@@ -89,10 +89,11 @@ module Proscenium
89
89
  # `packages: *list` as a flow list of the entries it names plus the pattern, so the anchored
90
90
  # list stays as it is for anything else that uses it.
91
91
  def spell_out(text, node)
92
- entries = ContextMap.yaml_packages(text)
92
+ entries = Contexts::Map.yaml_packages(text)
93
93
  raise refused unless entries&.all?(String)
94
94
 
95
- list = (entries + [ContextMap::REGISTRATION]).map { "'#{it.gsub("'", "''")}'" }.join(', ')
95
+ list = (entries + [Contexts::Map::REGISTRATION]).map { "'#{it.gsub("'", "''")}'" }
96
+ list = list.join(', ')
96
97
  start = offset(text, node)
97
98
  "#{text[0...start]}[#{list}]#{text[(start + node.end_column - node.start_column)..]}"
98
99
  end
@@ -107,16 +108,16 @@ module Proscenium
107
108
  elsif indent then "#{',' unless comma}#{eol}#{indent}"
108
109
  else comma ? ' ' : ', '
109
110
  end
110
- item = "'#{ContextMap::REGISTRATION}'#{',' if comma}"
111
+ item = "'#{Contexts::Map::REGISTRATION}'#{',' if comma}"
111
112
  text.dup.insert(start + list.length, "#{separator}#{item}")
112
113
  end
113
114
 
114
- # The `packages` list of a document that is empty or a mapping, as ContextMap reads it.
115
+ # The `packages` list of a document that is empty or a mapping, as Contexts::Map reads it.
115
116
  def packages(text)
116
117
  root = Psych.parse_stream(text).children.first&.root
117
118
  raise refused unless root.nil? || root.is_a?(Psych::Nodes::Mapping)
118
119
 
119
- ContextMap.yaml_packages(text)
120
+ Contexts::Map.yaml_packages(text)
120
121
  rescue Psych::SyntaxError
121
122
  raise refused
122
123
  end
@@ -125,7 +126,7 @@ module Proscenium
125
126
  def verify(text, spliced)
126
127
  before = packages(text)
127
128
  after = packages(spliced)
128
- expected = Array(before) + [ContextMap::REGISTRATION]
129
+ expected = Array(before) + [Contexts::Map::REGISTRATION]
129
130
  same = after.is_a?(Array) && after.tally == expected.tally
130
131
  raise refused unless same && others(spliced) == others(text)
131
132
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fileutils'
4
- require_relative '../context_map'
4
+ require_relative '../contexts/map'
5
5
 
6
6
  module Proscenium
7
7
  module CLI
@@ -41,8 +41,8 @@ module Proscenium
41
41
  end
42
42
 
43
43
  # The engine probes these two to refuse builds during an install, so both sides share them.
44
- def lock_path = File.join(@root, ContextMap::LOCK)
45
- def marker_path = File.join(@root, ContextMap::INSTALLING)
44
+ def lock_path = File.join(@root, Contexts::Map::LOCK)
45
+ def marker_path = File.join(@root, Contexts::Map::INSTALLING)
46
46
  def holder_path = File.join(@dir, 'holder')
47
47
  def manager_path = File.join(@dir, 'manager')
48
48
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
- require_relative '../context_map'
4
+ require_relative '../contexts/map'
5
5
  require_relative 'bun_linker'
6
6
  require_relative 'brackets'
7
7
  require_relative 'gitignore'
@@ -15,7 +15,7 @@ module Proscenium
15
15
  # splice, so the file's other settings, order and comments survive; each is computed without
16
16
  # writing, so `install` can print it as a diff first.
17
17
  module Registration
18
- PATTERN = ContextMap::REGISTRATION
18
+ PATTERN = Contexts::Map::REGISTRATION
19
19
 
20
20
  module_function
21
21
 
@@ -47,7 +47,7 @@ module Proscenium
47
47
  # Whether the app has adopted RubyGem NPM dependencies: its committed registration names the
48
48
  # contexts.
49
49
  def registered?(root, manager)
50
- ContextMap.registers?(root, manager == 'bun' ? 'package.json' : 'pnpm-workspace.yaml')
50
+ Contexts::Map.registers?(root, manager == 'bun' ? 'package.json' : 'pnpm-workspace.yaml')
51
51
  end
52
52
 
53
53
  def refused(file) = Error.new('PSM-E-REGISTRATION', file:)
@@ -56,11 +56,11 @@ module Proscenium
56
56
  # or adds the key. Refuses rather than guess at JSON it cannot splice. A byte order mark is
57
57
  # kept.
58
58
  def splice_package_json(text)
59
- return text if ContextMap.registered_in?('package.json', text, strict: true)
59
+ return text if Contexts::Map.registered_in?('package.json', text, strict: true)
60
60
 
61
61
  bom = text.start_with?("\uFEFF") ? "\uFEFF" : ''
62
62
  spliced = splice_json(text.delete_prefix(bom))
63
- if ContextMap.excludes?(ContextMap.json_workspaces(spliced))
63
+ if Contexts::Map.excludes?(Contexts::Map.json_workspaces(spliced))
64
64
  raise Error.new('PSM-E-REGISTRATION-EXCLUDED', file: 'package.json')
65
65
  end
66
66
 
@@ -37,9 +37,9 @@ module Proscenium
37
37
  end
38
38
 
39
39
  # A line of the human summary, or an event in JSON mode.
40
- def info(message, event: 'info', phase: nil, **details)
40
+ def info(message, event: 'info', **details)
41
41
  if @json
42
- emit(event:, phase:, status: 'ok', message:, details:)
42
+ emit(event:, phase: nil, status: 'ok', message:, details:)
43
43
  elsif !@quiet
44
44
  @out.puts message
45
45
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require_relative '../context_map'
3
+ require_relative '../contexts/map'
4
+ require_relative '../contexts/stale'
5
5
  require_relative 'manager'
6
6
 
7
7
  module Proscenium
@@ -12,7 +12,8 @@ module Proscenium
12
12
  class Verify
13
13
  # A `packages:` key in pnpm-lock.yaml, and a `packages` entry in bun.lock, for a
14
14
  # `@rubygems/*` package that is not a link to its context. A pnpm key starting with `@` is
15
- # always quoted, and a Git one holds colons: `'@rubygems/hue@git+ssh://git@github.com/...':`.
15
+ # always quoted, and a Git one holds colons:
16
+ # `'@rubygems/ui-gem@git+ssh://git@github.com/...':`.
16
17
  PNPM_REGISTRY = %r{^\s{2}'(@rubygems/[^@'\s]+)@(?!link:|workspace:)[^']*':\s*$}
17
18
  BUN_REGISTRY = %r{"(@rubygems/[^"]+)":\s*\["@rubygems/[^@"]+@(?!workspace:)}
18
19
 
@@ -40,7 +41,7 @@ module Proscenium
40
41
 
41
42
  def check_workspaces(lock)
42
43
  missing = @gems.reject do |gem|
43
- path = ".proscenium/packages/#{gem}"
44
+ path = "#{Contexts::Map::CONTEXTS}/#{gem}"
44
45
  if @manager.name == 'pnpm'
45
46
  # An importer with no dependencies is written on one line: ` <path>: {}`.
46
47
  lock.match?(/^ #{Regexp.escape(path)}:(?: \{\})?$/)
@@ -54,53 +55,11 @@ module Proscenium
54
55
  end
55
56
 
56
57
  # Each peer a context declares that the app also depends on must resolve, from the context
57
- # and from the app, to one real directory. A context that is missing or not valid JSON has
58
- # no peers to check; StaleContexts and `install --frozen` report it themselves.
58
+ # and from the app, to one real directory. The rule is the engine's too, so it lives with
59
+ # Contexts::Stale.
59
60
  def check_peers
60
- app = app_dependencies
61
- @gems.each do |gem|
62
- dir = File.join(@root, '.proscenium', 'packages', gem)
63
- (peers(dir).keys & app).each do |name|
64
- from_gem = resolve(dir, name)
65
- from_app = resolve(@root, name)
66
- next if from_gem.nil? || from_app.nil? || from_gem == from_app
67
-
68
- raise Error.new('PSM-E-PEER-SPLIT', gem:, package: name, manager: @manager.name)
69
- end
70
- end
71
- end
72
-
73
- def peers(dir)
74
- context = ContextMap.read_json(File.join(dir, 'package.json'))
75
- peers = context['peerDependencies'] if context.is_a?(Hash)
76
- peers.is_a?(Hash) ? peers : {}
77
- rescue Errno::ENOENT, JSON::ParserError
78
- {}
79
- end
80
-
81
- def app_dependencies
82
- package = ContextMap.read_json(File.join(@root, 'package.json'))
83
- return [] unless package.is_a?(Hash)
84
-
85
- # Peers too: pnpm installs the app's own peers at the root.
86
- %w[dependencies devDependencies optionalDependencies peerDependencies].flat_map do |field|
87
- package[field].is_a?(Hash) ? package[field].keys : []
88
- end
89
- rescue Errno::ENOENT, JSON::ParserError
90
- []
91
- end
92
-
93
- # The real directory `name` resolves to from `dir`, walking up node_modules as Node does,
94
- # but no higher than the app.
95
- def resolve(dir, name)
96
- current = dir
97
- loop do
98
- candidate = File.join(current, 'node_modules', name)
99
- return File.realpath(candidate) if File.exist?(candidate)
100
- return nil if current == @root || current == File.dirname(current)
101
-
102
- current = File.dirname(current)
103
- end
61
+ gem, package = Contexts::Stale.peer_split(@root, @gems)
62
+ raise Error.new('PSM-E-PEER-SPLIT', gem:, package:) if gem
104
63
  end
105
64
  end
106
65
  end
@@ -13,10 +13,11 @@ module Proscenium
13
13
  # The `proscenium` command (#154): installs participating gems' JavaScript dependencies through
14
14
  # the app's own package manager. It runs under `bundle exec` and reads the bundle in process.
15
15
  #
16
- # Load isolation is a contract, tested in test/cli/load_isolation_test.rb: nothing under
17
- # lib/proscenium/cli/ may require `proscenium` itself, ActiveSupport, Rails, FFI or the engine
18
- # library. Only Proscenium's plain-Ruby modules the engine shares with it (bundled_gems,
19
- # dependency_context, context_map, stale_contexts) and these files.
16
+ # Load isolation is a contract in both directions, tested in test/cli/load_isolation_test.rb.
17
+ # Nothing under lib/proscenium/cli/ may require `proscenium` itself, ActiveSupport, Rails, FFI or
18
+ # the engine library: only the plain-Ruby model the engine shares with it (bundled_gems and
19
+ # contexts/) and these files. And that model loads nothing of cli/, so a change to the package
20
+ # manager, install or verify cannot break the engine.
20
21
  module CLI
21
22
  USAGE = <<~TEXT
22
23
  Usage: bundle exec proscenium <command> [options]