proscenium 0.26.1-x86_64-linux-gnu → 0.27.0-x86_64-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.
- checksums.yaml +4 -4
- data/README.md +50 -3
- data/lib/proscenium/app_package.rb +8 -4
- data/lib/proscenium/builder/request.rb +72 -0
- data/lib/proscenium/builder.rb +10 -74
- data/lib/proscenium/cli/bun_linker.rb +2 -2
- data/lib/proscenium/cli/collisions.rb +7 -9
- data/lib/proscenium/cli/error.rb +58 -265
- data/lib/proscenium/cli/gem_archive.rb +4 -2
- data/lib/proscenium/cli/gem_check.rb +13 -11
- data/lib/proscenium/cli/gitignore.rb +2 -1
- data/lib/proscenium/cli/inspect.rb +12 -11
- data/lib/proscenium/cli/install.rb +35 -46
- data/lib/proscenium/cli/manager.rb +6 -6
- data/lib/proscenium/cli/platform_variants.rb +8 -8
- data/lib/proscenium/cli/pnpm_approvals.rb +3 -3
- data/lib/proscenium/cli/pnpm_workspace.rb +15 -14
- data/lib/proscenium/cli/project_lock.rb +3 -3
- data/lib/proscenium/cli/registration.rb +5 -5
- data/lib/proscenium/cli/reporter.rb +2 -2
- data/lib/proscenium/cli/verify.rb +9 -50
- data/lib/proscenium/cli.rb +5 -4
- data/lib/proscenium/contexts/map.rb +183 -0
- data/lib/proscenium/contexts/mapping_generation.rb +68 -0
- data/lib/proscenium/contexts/messages.rb +276 -0
- data/lib/proscenium/{cli/contexts.rb → contexts/plan.rb} +12 -12
- data/lib/proscenium/contexts/projection.rb +77 -0
- data/lib/proscenium/{cli → contexts}/rules.rb +1 -1
- data/lib/proscenium/contexts/stale.rb +174 -0
- data/lib/proscenium/contexts.rb +16 -0
- data/lib/proscenium/css_module/path.rb +1 -0
- data/lib/proscenium/css_module/transformer.rb +15 -0
- data/lib/proscenium/css_module.rb +3 -2
- data/lib/proscenium/ext/proscenium +0 -0
- data/lib/proscenium/helper.rb +6 -1
- data/lib/proscenium/importer.rb +13 -1
- data/lib/proscenium/middleware/base.rb +0 -4
- data/lib/proscenium/middleware.rb +0 -2
- data/lib/proscenium/railtie.rb +4 -4
- data/lib/proscenium/resolver.rb +3 -1
- data/lib/proscenium/side_load.rb +6 -2
- data/lib/proscenium/source_path.rb +2 -0
- data/lib/proscenium/utils.rb +2 -1
- data/lib/proscenium/version.rb +1 -1
- data/lib/proscenium.rb +5 -5
- metadata +10 -7
- data/lib/proscenium/context_map.rb +0 -181
- data/lib/proscenium/dependency_context.rb +0 -74
- data/lib/proscenium/mapping_generation.rb +0 -64
- 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 '../
|
|
7
|
-
require_relative '../
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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,
|
|
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'] ==
|
|
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::
|
|
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::
|
|
156
|
-
[File.join(Contexts::
|
|
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',
|
|
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(
|
|
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',
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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::
|
|
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::
|
|
306
|
-
"#{Contexts::
|
|
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
|
|
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
|
|
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
|
|
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=!#{
|
|
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 '../
|
|
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 =
|
|
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) &&
|
|
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 =
|
|
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) ?
|
|
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) &&
|
|
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 '../
|
|
6
|
-
require_relative '../
|
|
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 =
|
|
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
|
-
|
|
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 &&
|
|
73
|
+
manifest = text && Contexts::Map.parse_json(text)
|
|
74
74
|
return nil unless manifest.is_a?(Hash)
|
|
75
75
|
|
|
76
|
-
|
|
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 '../
|
|
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 =
|
|
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 =
|
|
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 '../
|
|
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
|
|
25
|
+
return text if Contexts::Map.registered_in?('pnpm-workspace.yaml', text, strict: true)
|
|
26
26
|
|
|
27
|
-
text =
|
|
28
|
-
bom = text.start_with?(
|
|
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
|
|
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}- #{
|
|
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} - #{
|
|
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
|
|
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 =
|
|
92
|
+
entries = Contexts::Map.yaml_packages(text)
|
|
93
93
|
raise refused unless entries&.all?(String)
|
|
94
94
|
|
|
95
|
-
list = (entries + [
|
|
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 = "'#{
|
|
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
|
|
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
|
-
|
|
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) + [
|
|
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 '../
|
|
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,
|
|
45
|
-
def marker_path = File.join(@root,
|
|
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 '../
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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',
|
|
40
|
+
def info(message, event: 'info', **details)
|
|
41
41
|
if @json
|
|
42
|
-
emit(event:, phase
|
|
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
|
-
|
|
4
|
-
require_relative '../
|
|
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:
|
|
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 = "
|
|
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.
|
|
58
|
-
#
|
|
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
|
-
|
|
61
|
-
|
|
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
|
data/lib/proscenium/cli.rb
CHANGED
|
@@ -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
|
|
17
|
-
# lib/proscenium/cli/ may require `proscenium` itself, ActiveSupport, Rails, FFI or
|
|
18
|
-
# library
|
|
19
|
-
#
|
|
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]
|