proscenium 0.26.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.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +1020 -0
- data/exe/proscenium +19 -0
- data/exe/psm +5 -0
- data/lib/generators/proscenium/bun/bun_generator.rb +150 -0
- data/lib/generators/proscenium/bun/templates/proscenium.preload.js +61 -0
- data/lib/proscenium/app_package.rb +90 -0
- data/lib/proscenium/builder.rb +403 -0
- data/lib/proscenium/bundled_gems.rb +140 -0
- data/lib/proscenium/cli/brackets.rb +66 -0
- data/lib/proscenium/cli/bun_linker.rb +79 -0
- data/lib/proscenium/cli/capabilities.json +29 -0
- data/lib/proscenium/cli/collisions.rb +94 -0
- data/lib/proscenium/cli/contexts.rb +133 -0
- data/lib/proscenium/cli/diff.rb +80 -0
- data/lib/proscenium/cli/error.rb +291 -0
- data/lib/proscenium/cli/gem_archive.rb +44 -0
- data/lib/proscenium/cli/gem_check.rb +150 -0
- data/lib/proscenium/cli/gitignore.rb +63 -0
- data/lib/proscenium/cli/inspect.rb +257 -0
- data/lib/proscenium/cli/install.rb +378 -0
- data/lib/proscenium/cli/manager.rb +323 -0
- data/lib/proscenium/cli/platform_variants.rb +82 -0
- data/lib/proscenium/cli/pnpm_approvals.rb +109 -0
- data/lib/proscenium/cli/pnpm_workspace.rb +149 -0
- data/lib/proscenium/cli/project_lock.rb +106 -0
- data/lib/proscenium/cli/registration.rb +140 -0
- data/lib/proscenium/cli/reporter.rb +103 -0
- data/lib/proscenium/cli/rules.rb +169 -0
- data/lib/proscenium/cli/runner.rb +142 -0
- data/lib/proscenium/cli/verify.rb +107 -0
- data/lib/proscenium/cli.rb +122 -0
- data/lib/proscenium/context_map.rb +181 -0
- data/lib/proscenium/css_module/path.rb +32 -0
- data/lib/proscenium/css_module/transformer.rb +118 -0
- data/lib/proscenium/css_module.rb +63 -0
- data/lib/proscenium/dependency_context.rb +74 -0
- data/lib/proscenium/ensure_loaded.rb +31 -0
- data/lib/proscenium/error.rb +10 -0
- data/lib/proscenium/ext/proscenium +0 -0
- data/lib/proscenium/ext/proscenium.h +113 -0
- data/lib/proscenium/helper.rb +82 -0
- data/lib/proscenium/importer.rb +189 -0
- data/lib/proscenium/log_subscriber.rb +38 -0
- data/lib/proscenium/manifest.rb +48 -0
- data/lib/proscenium/mapping_generation.rb +64 -0
- data/lib/proscenium/middleware/base.rb +102 -0
- data/lib/proscenium/middleware/chunks.rb +46 -0
- data/lib/proscenium/middleware/esbuild.rb +11 -0
- data/lib/proscenium/middleware/ruby_gems.rb +37 -0
- data/lib/proscenium/middleware/silence_request.rb +31 -0
- data/lib/proscenium/middleware/vendor.rb +31 -0
- data/lib/proscenium/middleware.rb +67 -0
- data/lib/proscenium/monkey.rb +77 -0
- data/lib/proscenium/railtie.rb +106 -0
- data/lib/proscenium/railties/assets.rake +19 -0
- data/lib/proscenium/resolver.rb +67 -0
- data/lib/proscenium/runtime/bootstrap.js +266 -0
- data/lib/proscenium/runtime/bun.js +143 -0
- data/lib/proscenium/runtime/server.rb +702 -0
- data/lib/proscenium/side_load.rb +210 -0
- data/lib/proscenium/source_path.rb +15 -0
- data/lib/proscenium/stale_contexts.rb +118 -0
- data/lib/proscenium/templates/rescues/build_error.html.erb +32 -0
- data/lib/proscenium/utils.rb +104 -0
- data/lib/proscenium/version.rb +5 -0
- data/lib/proscenium/yaml_tree.rb +48 -0
- data/lib/proscenium.rb +87 -0
- metadata +148 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"comment": "The pnpm and Bun lines Proscenium supports (#154). The source of truth: the CLI checks manager versions against it, and CI pins the versions it lists. `ci` is the exact version CI runs; `eol` is when the line's owner stops maintaining it. pnpm 10 is not supported (decided 4 October 2026): it leaves a context on an older copy of a shared peer after the app upgrades it, and 11 and 12 do not. pnpm 11 starts at 11.11.0: earlier 11.x let a package spoofing an approved name in its manifest run its build scripts (GHSA-5wx6-mg75-v57r, fixed in 11.5.3) and a tarball's manifest name write outside node_modules (GHSA-vq4v-j7r6-jq4m, fixed in 11.11.0).",
|
|
4
|
+
"managers": {
|
|
5
|
+
"pnpm": {
|
|
6
|
+
"lines": [
|
|
7
|
+
{
|
|
8
|
+
"range": ">= 11.11.0, < 12",
|
|
9
|
+
"ci": "11.28.4",
|
|
10
|
+
"eol": "2027-04-30"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"range": ">= 12.0.0, < 13",
|
|
14
|
+
"ci": "12.9.1",
|
|
15
|
+
"eol": null
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"bun": {
|
|
20
|
+
"lines": [
|
|
21
|
+
{
|
|
22
|
+
"range": ">= 1.4.0, < 1.5",
|
|
23
|
+
"ci": "1.4.2",
|
|
24
|
+
"eol": null
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require_relative 'registration'
|
|
6
|
+
|
|
7
|
+
module Proscenium
|
|
8
|
+
module CLI
|
|
9
|
+
# The app's own packages that already take a gem context's name, `@rubygems/<gem>` (#154,
|
|
10
|
+
# C05): a dependency on it that is not the context, such as the GitHub pin an app used before
|
|
11
|
+
# adopting, and a workspace package so named. Install reads them before it writes anything.
|
|
12
|
+
module Collisions
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
DEPENDENCY_FIELDS = %w[dependencies devDependencies optionalDependencies
|
|
16
|
+
peerDependencies].freeze
|
|
17
|
+
|
|
18
|
+
# One line for each package of the app's own taking a context's name.
|
|
19
|
+
def find(root, manager, gems)
|
|
20
|
+
names = gems.to_h { ["@rubygems/#{it}", it] }
|
|
21
|
+
app = read_json(File.join(root, 'package.json')) || {}
|
|
22
|
+
pinned = DEPENDENCY_FIELDS.flat_map do |field|
|
|
23
|
+
(app[field] || {}).filter_map do |name, spec|
|
|
24
|
+
next if !names.key?(name) || context_workspace?(spec, name) ||
|
|
25
|
+
context_link?(root, spec, names[name])
|
|
26
|
+
|
|
27
|
+
"package.json #{field} has #{name} as \"#{spec}\""
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
pinned + workspace_packages(root, manager, app).filter_map do |file, name|
|
|
31
|
+
"#{file} is named #{name}" if names.key?(name)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Whether `spec` is the workspace package `name`, the context: `workspace:<range>`, not an
|
|
36
|
+
# alias to another package, `workspace:<other>@<range>`.
|
|
37
|
+
def context_workspace?(spec, name)
|
|
38
|
+
range = spec.to_s.delete_prefix('workspace:')
|
|
39
|
+
return false if range == spec.to_s
|
|
40
|
+
|
|
41
|
+
aliased = range[/\A(@?[^@]+)@/, 1]
|
|
42
|
+
aliased.nil? || aliased == name
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Whether `spec` links to the gem's own context, however the path is spelled.
|
|
46
|
+
def context_link?(root, spec, gem)
|
|
47
|
+
return false unless spec.to_s.start_with?('link:')
|
|
48
|
+
|
|
49
|
+
context = File.join(File.expand_path(root), ContextMap::CONTEXTS, gem)
|
|
50
|
+
File.expand_path(spec.to_s.delete_prefix('link:'), root) == context
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# [package.json path, package name] for each workspace package the app declares, other than
|
|
54
|
+
# the contexts. A pattern starting with `!` excludes. `./` is dropped from each, as pnpm and
|
|
55
|
+
# Bun read `./packages/*` and `packages/*` alike.
|
|
56
|
+
def workspace_packages(root, manager, app)
|
|
57
|
+
excluded, included = workspace_patterns(root, manager, app)
|
|
58
|
+
.grep(String).reject { ContextMap.registration?(it) }
|
|
59
|
+
.map { it.sub(%r{\A(!?)\./}, '\\1') }
|
|
60
|
+
.partition { it.start_with?('!') }
|
|
61
|
+
files = included.flat_map { Dir.glob(File.join(it, 'package.json'), base: root) }.uniq
|
|
62
|
+
files.filter_map do |file|
|
|
63
|
+
dir = File.dirname(file)
|
|
64
|
+
next if dir.split('/').include?('node_modules')
|
|
65
|
+
|
|
66
|
+
# FNM_EXTGLOB for braces, which pnpm and Bun expand, as Dir.glob does for the inclusions.
|
|
67
|
+
flags = File::FNM_PATHNAME | File::FNM_EXTGLOB
|
|
68
|
+
next if excluded.any? { File.fnmatch?(it[1..], dir, flags) }
|
|
69
|
+
|
|
70
|
+
[file, read_json(File.join(root, file))&.dig('name')]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def workspace_patterns(root, manager, app)
|
|
75
|
+
if manager == 'bun'
|
|
76
|
+
workspaces = app['workspaces']
|
|
77
|
+
Array(workspaces.is_a?(Hash) ? workspaces['packages'] : workspaces)
|
|
78
|
+
else
|
|
79
|
+
path = File.join(root, 'pnpm-workspace.yaml')
|
|
80
|
+
File.exist?(path) ? Array(ContextMap.yaml_packages(File.read(path))) : []
|
|
81
|
+
end
|
|
82
|
+
rescue Psych::SyntaxError
|
|
83
|
+
[] # the manager reports it
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def read_json(path)
|
|
87
|
+
json = File.exist?(path) && ContextMap.read_json(path)
|
|
88
|
+
json.is_a?(Hash) ? json : nil
|
|
89
|
+
rescue JSON::ParserError
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative '../context_map'
|
|
5
|
+
require_relative '../bundled_gems'
|
|
6
|
+
require_relative '../dependency_context'
|
|
7
|
+
require_relative 'rules'
|
|
8
|
+
|
|
9
|
+
module Proscenium
|
|
10
|
+
module CLI
|
|
11
|
+
# The contexts the bundle calls for (#154): one per participating, installed gem, projected
|
|
12
|
+
# from its package.json after the author contract and the gem-to-gem rules. Computes; never
|
|
13
|
+
# writes.
|
|
14
|
+
class Contexts
|
|
15
|
+
DIR = File.join('.proscenium', 'packages')
|
|
16
|
+
ESCAPE = "Or, to install %<gem>s's NPM dependencies yourself, opt it out with " \
|
|
17
|
+
'`"proscenium": {"gemOverrides": {"%<gem>s": {"participate": false}}}` in ' \
|
|
18
|
+
"package.json, then add the packages it needs to your app's own dependencies."
|
|
19
|
+
|
|
20
|
+
# A gem's context: its JSON text, and the Git and URL dependencies it introduces.
|
|
21
|
+
Context = Struct.new(:gem, :json, :git_and_url, keyword_init: true)
|
|
22
|
+
|
|
23
|
+
attr_reader :contexts, :problems, :warnings
|
|
24
|
+
|
|
25
|
+
# `specs` are the participating installed specs by name; `root` is the app.
|
|
26
|
+
def initialize(root, specs)
|
|
27
|
+
@root = root
|
|
28
|
+
@specs = specs
|
|
29
|
+
@contexts = {}
|
|
30
|
+
@problems = []
|
|
31
|
+
@warnings = []
|
|
32
|
+
specs.each_value { add(it) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ok? = @problems.empty?
|
|
36
|
+
|
|
37
|
+
# The committed context directories under the app, by gem name.
|
|
38
|
+
def self.committed(root)
|
|
39
|
+
dir = File.join(root, DIR)
|
|
40
|
+
return {} unless Dir.exist?(dir)
|
|
41
|
+
|
|
42
|
+
Dir.children(dir).select { File.directory?(File.join(dir, it)) }.sort
|
|
43
|
+
.to_h { [it, File.join(dir, it, 'package.json')] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def add(spec)
|
|
49
|
+
gem = spec.name
|
|
50
|
+
manifest_root = BundledGems.manifest_root(spec)
|
|
51
|
+
unless manifest_root
|
|
52
|
+
return problem('PSM-E-FRONTEND-ROOT', gem:,
|
|
53
|
+
root: spec.metadata['proscenium.frontend_root'])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
manifest = read_manifest(gem, manifest_root, spec.full_gem_path) or return
|
|
57
|
+
fatal = Rules.check(gem, manifest, root: manifest_root)
|
|
58
|
+
warnings, fatal = fatal.partition { Rules::WARNINGS.include?(it.first) }
|
|
59
|
+
@warnings.concat(warnings)
|
|
60
|
+
fatal.each { |code, args| problem(code, **args) }
|
|
61
|
+
return unless fatal.empty?
|
|
62
|
+
|
|
63
|
+
manifest = link_gems(gem, spec, manifest) or return
|
|
64
|
+
context = DependencyContext.project(gem, manifest)
|
|
65
|
+
@contexts[gem] = Context.new(gem:, json: DependencyContext.to_json(context),
|
|
66
|
+
git_and_url: git_and_url(context))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def read_manifest(gem, manifest_root, gem_root)
|
|
70
|
+
path = File.join(manifest_root, 'package.json')
|
|
71
|
+
unless File.exist?(path)
|
|
72
|
+
return problem('PSM-E-MANIFEST', gem:, path: 'package.json', cause: 'it is missing')
|
|
73
|
+
end
|
|
74
|
+
if (cause = Rules.manifest_file_problem(path, gem_root))
|
|
75
|
+
return problem('PSM-E-MANIFEST', gem:, path: 'package.json', cause:)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
manifest = ContextMap.read_json(path)
|
|
79
|
+
return manifest if manifest.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
problem('PSM-E-MANIFEST', gem:, path: 'package.json', cause: 'it is not a JSON object')
|
|
82
|
+
rescue JSON::ParserError
|
|
83
|
+
problem('PSM-E-MANIFEST', gem:, path: 'package.json', cause: 'it is not valid JSON')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Rewrites references to other gems' contexts to `workspace:*`. A reference is kept only if
|
|
87
|
+
# the gemspec depends on the other gem and that gem participates: Bundler then guarantees it
|
|
88
|
+
# is locked at a compatible version. An optional peer whose gem does not participate is left
|
|
89
|
+
# out, as an absent optional peer would be.
|
|
90
|
+
def link_gems(gem, spec, manifest)
|
|
91
|
+
runtime = spec.runtime_dependencies.map(&:name)
|
|
92
|
+
optional = (manifest['peerDependenciesMeta'] || {}).select do |_, meta|
|
|
93
|
+
meta.is_a?(Hash) && meta['optional']
|
|
94
|
+
end.keys
|
|
95
|
+
ok = true
|
|
96
|
+
linked = Rules::DEPENDENCY_FIELDS.to_h do |field|
|
|
97
|
+
deps = (manifest[field] || {}).filter_map do |name, version|
|
|
98
|
+
next [name, version] unless name.start_with?('@rubygems/')
|
|
99
|
+
|
|
100
|
+
other = name.delete_prefix('@rubygems/')
|
|
101
|
+
participates = @specs.key?(other)
|
|
102
|
+
if field == 'peerDependencies' && optional.include?(name) && !participates
|
|
103
|
+
nil
|
|
104
|
+
elsif !runtime.include?(other)
|
|
105
|
+
ok = false
|
|
106
|
+
problem('PSM-E-CROSS-GEM', gem:, other:)
|
|
107
|
+
elsif !participates
|
|
108
|
+
ok = false
|
|
109
|
+
problem('PSM-E-CROSS-GEM-TARGET', gem:, other:)
|
|
110
|
+
else
|
|
111
|
+
[name, 'workspace:*']
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
[field, deps.to_h]
|
|
115
|
+
end
|
|
116
|
+
ok ? manifest.merge(linked) : nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def git_and_url(context)
|
|
120
|
+
Rules::DEPENDENCY_FIELDS.flat_map { (context[it] || {}).to_a }
|
|
121
|
+
.select { |_, spec| spec.to_s.match?(%r{\A(?:github:|git\+|https://)}) }
|
|
122
|
+
.map { |name, spec| "#{name}@#{spec}" }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Records a problem with the gem's own manifest, with the consumer's way out. Returns nil,
|
|
126
|
+
# so a caller can `return problem(...)` to stop.
|
|
127
|
+
def problem(code, **args)
|
|
128
|
+
@problems << [code, args.merge(escape: format(ESCAPE, gem: args.fetch(:gem)))]
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proscenium
|
|
4
|
+
module CLI
|
|
5
|
+
# A unified diff of a small text file's edit, which `install` prints before it writes one of
|
|
6
|
+
# the app's files. Line-based, over a longest common subsequence: fine for the files it is
|
|
7
|
+
# used on, which are a few dozen lines.
|
|
8
|
+
module Diff
|
|
9
|
+
MARKS = { same: ' ', del: '-', add: '+' }.freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# The diff of `before` to `after`, headed with `path`; empty when they are the same.
|
|
14
|
+
def unified(path, before, after, context: 3)
|
|
15
|
+
ops = lcs_ops(before.lines(chomp: true), after.lines(chomp: true))
|
|
16
|
+
hunks = hunks(ops, context)
|
|
17
|
+
return '' if hunks.empty?
|
|
18
|
+
|
|
19
|
+
header = "--- #{path}\n+++ #{path}\n"
|
|
20
|
+
header + hunks.map { |hunk| format_hunk(hunk) }.join
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Each line as [:same | :del | :add, text, line in old, line in new].
|
|
24
|
+
def lcs_ops(old, new)
|
|
25
|
+
table = lcs_table(old, new)
|
|
26
|
+
ops = []
|
|
27
|
+
i = j = 0
|
|
28
|
+
while i < old.size || j < new.size
|
|
29
|
+
if i < old.size && j < new.size && old[i] == new[j]
|
|
30
|
+
ops << [:same, old[i], i, j]
|
|
31
|
+
i += 1
|
|
32
|
+
j += 1
|
|
33
|
+
elsif j < new.size && (i == old.size || table[i][j + 1] >= table[i + 1][j])
|
|
34
|
+
ops << [:add, new[j], i, j]
|
|
35
|
+
j += 1
|
|
36
|
+
else
|
|
37
|
+
ops << [:del, old[i], i, j]
|
|
38
|
+
i += 1
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
ops
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The longest common subsequence lengths of every pair of suffixes.
|
|
45
|
+
def lcs_table(old, new)
|
|
46
|
+
table = Array.new(old.size + 1) { Array.new(new.size + 1, 0) }
|
|
47
|
+
(old.size - 1).downto(0) do |i|
|
|
48
|
+
(new.size - 1).downto(0) do |j|
|
|
49
|
+
table[i][j] = if old[i] == new[j]
|
|
50
|
+
table[i + 1][j + 1] + 1
|
|
51
|
+
else
|
|
52
|
+
[table[i + 1][j], table[i][j + 1]].max
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
table
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def hunks(ops, context)
|
|
60
|
+
changed = ops.each_index.reject { ops[it][0] == :same }
|
|
61
|
+
return [] if changed.empty?
|
|
62
|
+
|
|
63
|
+
groups = changed.slice_when { |x, y| y - x > context * 2 }
|
|
64
|
+
groups.map do |group|
|
|
65
|
+
ops[[group.first - context, 0].max..[group.last + context, ops.size - 1].min]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def format_hunk(ops)
|
|
70
|
+
a_start = ops.first[2] + 1
|
|
71
|
+
b_start = ops.first[3] + 1
|
|
72
|
+
a_count = ops.count { it[0] != :add }
|
|
73
|
+
b_count = ops.count { it[0] != :del }
|
|
74
|
+
lines = ops.map { |op, text| "#{MARKS.fetch(op)}#{text}\n" }
|
|
75
|
+
"@@ -#{a_count.zero? ? a_start - 1 : a_start},#{a_count} " \
|
|
76
|
+
"+#{b_count.zero? ? b_start - 1 : b_start},#{b_count} @@\n#{lines.join}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Proscenium
|
|
4
|
+
module CLI
|
|
5
|
+
# Every failure the CLI reports: a stable code, the exit status it maps to, and text that states
|
|
6
|
+
# the problem, its cause and the fix. The catalog is the single source; golden-output tests pin
|
|
7
|
+
# the human text and the JSON event for each entry (test/cli/golden/).
|
|
8
|
+
class Error < StandardError
|
|
9
|
+
EXIT = { success: 0, internal: 1, input: 2, unsupported: 3, drift: 4, integrity: 5,
|
|
10
|
+
native: 6, busy: 7, interrupted: 8 }.freeze
|
|
11
|
+
|
|
12
|
+
# code => [exit, message template, fix template]. Templates take keyword arguments.
|
|
13
|
+
CATALOG = {
|
|
14
|
+
'PSM-E-USAGE' => [
|
|
15
|
+
:input, '%<detail>s',
|
|
16
|
+
'Run `bundle exec proscenium --help` to see the commands and their options.'
|
|
17
|
+
],
|
|
18
|
+
'PSM-E-GEMSPEC' => [
|
|
19
|
+
:input, 'Expected one gemspec in %<path>s, but found %<found>s.',
|
|
20
|
+
"Run `proscenium gem check` from the gem's own directory, or pass it that directory " \
|
|
21
|
+
'or a built .gem file.'
|
|
22
|
+
],
|
|
23
|
+
'PSM-E-NOT-OPTED-IN' => [
|
|
24
|
+
:input, '%<gem>s has not opted in to having Proscenium install its NPM ' \
|
|
25
|
+
'dependencies.',
|
|
26
|
+
"Add `spec.metadata['proscenium.dependencies'] = 'true'` to its gemspec."
|
|
27
|
+
],
|
|
28
|
+
'PSM-E-FRONTEND-ROOT' => [
|
|
29
|
+
:input, "%<gem>s's proscenium.frontend_root (%<root>s) points outside the gem.",
|
|
30
|
+
"Set proscenium.frontend_root to a directory inside the gem, relative to the gem's root."
|
|
31
|
+
],
|
|
32
|
+
'PSM-E-MANIFEST' => [
|
|
33
|
+
:input, "%<gem>s's %<path>s can't be used, because %<cause>s.",
|
|
34
|
+
'Ship a valid package.json there: a JSON object whose dependency fields map package ' \
|
|
35
|
+
'names to version strings.'
|
|
36
|
+
],
|
|
37
|
+
'PSM-E-GEM-FILES' => [
|
|
38
|
+
:input, "%<gem>s's gemspec doesn't list %<files>s in spec.files, so the built gem " \
|
|
39
|
+
"won't include it.",
|
|
40
|
+
'Add package.json, and every frontend file the gem serves, to spec.files.'
|
|
41
|
+
],
|
|
42
|
+
'PSM-E-NAME' => [
|
|
43
|
+
:input, "The gem name %<gem>s can't be used as a JavaScript package name " \
|
|
44
|
+
'(@rubygems/%<gem>s).',
|
|
45
|
+
'JavaScript package names use only lowercase letters, digits and - . _ ~, are at most ' \
|
|
46
|
+
"214 characters long, and can't end in a dot or be a Windows device name such as con " \
|
|
47
|
+
'or nul. The gem needs a valid name before Proscenium can install its dependencies.'
|
|
48
|
+
],
|
|
49
|
+
'PSM-E-WORKSPACES' => [
|
|
50
|
+
:input, "%<gem>s's package.json declares workspaces, which a gem's package.json " \
|
|
51
|
+
"can't do.",
|
|
52
|
+
"Remove \"workspaces\" from the gem's package.json."
|
|
53
|
+
],
|
|
54
|
+
'PSM-E-HOOK' => [
|
|
55
|
+
:input, '%<gem>s needs build steps at install time (%<hooks>s), and Proscenium never ' \
|
|
56
|
+
'runs them.',
|
|
57
|
+
"Ship the gem's assets already built, and remove those install-time hooks."
|
|
58
|
+
],
|
|
59
|
+
'PSM-E-SPEC' => [
|
|
60
|
+
:input, "%<gem>s depends on %<name>s as \"%<spec>s\", which Proscenium doesn't allow.",
|
|
61
|
+
'Use a version range or tag, an npm:<package> alias, a github:, git+https: or git+ssh: ' \
|
|
62
|
+
'URL, or an https: tarball URL.'
|
|
63
|
+
],
|
|
64
|
+
'PSM-E-CREDENTIAL' => [
|
|
65
|
+
:input, "%<gem>s's dependency %<name>s has a password or token in its URL, which " \
|
|
66
|
+
'would be committed to your repository.',
|
|
67
|
+
'Remove it from the URL. Apps log in to private registries in their own .npmrc.'
|
|
68
|
+
],
|
|
69
|
+
'PSM-E-ALIAS' => [
|
|
70
|
+
:input, "%<gem>s aliases %<name>s to \"%<spec>s\", which is another gem's " \
|
|
71
|
+
'dependency context.',
|
|
72
|
+
"Add the other gem to the gemspec's dependencies, and use @rubygems/<gem> directly."
|
|
73
|
+
],
|
|
74
|
+
'PSM-E-REACT' => [
|
|
75
|
+
:input, '%<gem>s lists %<packages>s as dependencies, so it would get its own copy and ' \
|
|
76
|
+
'the page would load two.',
|
|
77
|
+
"List %<packages>s under peerDependencies instead, so the gem uses the app's copy."
|
|
78
|
+
],
|
|
79
|
+
'PSM-E-CROSS-GEM' => [
|
|
80
|
+
:input, "%<gem>s's package.json uses @rubygems/%<other>s, but its gemspec doesn't " \
|
|
81
|
+
'depend on the %<other>s gem.',
|
|
82
|
+
"Add `spec.add_dependency '%<other>s'` to the gemspec, or stop using " \
|
|
83
|
+
'@rubygems/%<other>s.'
|
|
84
|
+
],
|
|
85
|
+
'PSM-E-NO-MANAGER' => [
|
|
86
|
+
:input, "Proscenium can't tell which package manager this app uses: there is no " \
|
|
87
|
+
'packageManager field in package.json, and no pnpm-lock.yaml or bun.lock.',
|
|
88
|
+
'Run again with --manager pnpm or --manager bun, or add a packageManager field to ' \
|
|
89
|
+
'package.json.'
|
|
90
|
+
],
|
|
91
|
+
'PSM-E-MANAGER-CONFLICT' => [
|
|
92
|
+
:input, 'This app points to more than one package manager: %<signals>s.',
|
|
93
|
+
'Keep just one: delete the other lockfile, or make packageManager in package.json ' \
|
|
94
|
+
'match the lockfile. (--manager only decides when nothing else does.)'
|
|
95
|
+
],
|
|
96
|
+
'PSM-E-UNSUPPORTED-MANAGER' => [
|
|
97
|
+
:unsupported, 'This app uses %<manager>s, but Proscenium can only install ' \
|
|
98
|
+
"gems' NPM dependencies with pnpm or Bun.",
|
|
99
|
+
"Switch the app to pnpm or Bun, or keep adding gems' NPM dependencies to " \
|
|
100
|
+
'package.json yourself.'
|
|
101
|
+
],
|
|
102
|
+
'PSM-E-BUN-LOCKB' => [
|
|
103
|
+
:unsupported, "This app only has bun.lockb, Bun's older binary lockfile, which " \
|
|
104
|
+
"Proscenium can't read.",
|
|
105
|
+
'Run `bun install --save-text-lockfile` to write bun.lock, then commit it.'
|
|
106
|
+
],
|
|
107
|
+
'PSM-E-MANAGER-MISSING' => [
|
|
108
|
+
:unsupported, "%<manager>s isn't installed, or isn't on your PATH.",
|
|
109
|
+
'Install %<manager>s the way this project normally does (Corepack, mise, Volta or its ' \
|
|
110
|
+
'own installer), then try again.'
|
|
111
|
+
],
|
|
112
|
+
'PSM-E-MANAGER-VERSION' => [
|
|
113
|
+
:unsupported, "Proscenium doesn't support %<manager>s %<version>s.",
|
|
114
|
+
'Use a supported version of %<manager>s (%<supported>s). To experiment locally with ' \
|
|
115
|
+
'another, add --experimental-manager-version.'
|
|
116
|
+
],
|
|
117
|
+
'PSM-E-EXPERIMENTAL-FROZEN' => [
|
|
118
|
+
:unsupported, "--experimental-manager-version can't be combined with --frozen.",
|
|
119
|
+
'Use a supported package manager version wherever --frozen runs, such as CI.'
|
|
120
|
+
],
|
|
121
|
+
'PSM-E-BUN-LINKER' => [
|
|
122
|
+
:unsupported, "bunfig.toml doesn't say which linker Bun uses (hoisted or isolated), " \
|
|
123
|
+
'and adding gem dependency contexts could make Bun switch linkers.',
|
|
124
|
+
'Add `linker = "hoisted"` (or "isolated", whichever your app uses today) to the ' \
|
|
125
|
+
'`[install]` table in bunfig.toml, creating the table if there is none.'
|
|
126
|
+
],
|
|
127
|
+
'PSM-E-NESTED-WORKSPACE' => [
|
|
128
|
+
:unsupported, 'This app is inside the JavaScript workspace at %<enclosing>s, and ' \
|
|
129
|
+
"Proscenium doesn't support that yet.",
|
|
130
|
+
"Keep adding gems' NPM dependencies to that workspace yourself, as you do today."
|
|
131
|
+
],
|
|
132
|
+
'PSM-E-REGISTRATION' => [
|
|
133
|
+
:input, "Proscenium couldn't add the gem dependency contexts to %<file>s without " \
|
|
134
|
+
'rewriting the file.',
|
|
135
|
+
'Check that %<file>s is valid (package.json must be strict JSON, with no comments or ' \
|
|
136
|
+
'trailing commas). Then add ".proscenium/packages/*" yourself, to `packages` in ' \
|
|
137
|
+
'pnpm-workspace.yaml or `workspaces` in package.json, and run ' \
|
|
138
|
+
'`bundle exec proscenium install` again.'
|
|
139
|
+
],
|
|
140
|
+
'PSM-E-REGISTRATION-EXCLUDED' => [
|
|
141
|
+
:input, '%<file>s excludes .proscenium/packages/ with a "!" pattern, so your package ' \
|
|
142
|
+
'manager would never install the gem dependency contexts.',
|
|
143
|
+
'Remove that pattern from %<file>s, then run `bundle exec proscenium install` again.'
|
|
144
|
+
],
|
|
145
|
+
'PSM-E-BUSY' => [
|
|
146
|
+
:busy, '%<holder>s is already running in this project.',
|
|
147
|
+
'Wait for it to finish, then run your command again. If nothing is actually ' \
|
|
148
|
+
'installing, delete .proscenium/manager, which an earlier run left behind.'
|
|
149
|
+
],
|
|
150
|
+
'PSM-E-NATIVE' => [
|
|
151
|
+
:native, '`%<command>s` failed (exit status %<status>s).',
|
|
152
|
+
'Its output is above. Fix what it reports, then run `bundle exec proscenium install` ' \
|
|
153
|
+
"again. Proscenium hasn't changed your package manager or regenerated your lockfile."
|
|
154
|
+
],
|
|
155
|
+
'PSM-E-INTERRUPTED' => [
|
|
156
|
+
:interrupted, '`%<command>s` was stopped before it finished.',
|
|
157
|
+
'Run `bundle exec proscenium install` again to finish.'
|
|
158
|
+
],
|
|
159
|
+
'PSM-E-CROSS-GEM-TARGET' => [
|
|
160
|
+
:input, "%<gem>s's package.json uses @rubygems/%<other>s, but %<other>s hasn't " \
|
|
161
|
+
'opted in to having Proscenium install its NPM dependencies.',
|
|
162
|
+
'Opt %<other>s in: its author adds proscenium.dependencies to its gemspec, or you add ' \
|
|
163
|
+
'`"proscenium": {"gemOverrides": {"%<other>s": {"participate": true}}}` to your ' \
|
|
164
|
+
'package.json.'
|
|
165
|
+
],
|
|
166
|
+
'PSM-E-CONFIG' => [
|
|
167
|
+
:input, "Proscenium can't read its settings: %<detail>s.",
|
|
168
|
+
'Fix package.json. Proscenium reads one setting from it: ' \
|
|
169
|
+
'`"proscenium": {"gemOverrides": {"<gem>": {"participate": true}}}`, with true or ' \
|
|
170
|
+
'false for each gem.'
|
|
171
|
+
],
|
|
172
|
+
'PSM-E-PROBLEMS' => [
|
|
173
|
+
:input, 'The install stopped because of the problems above. Nothing was changed.',
|
|
174
|
+
'Fix each one as it says, then run `bundle exec proscenium install` again.'
|
|
175
|
+
],
|
|
176
|
+
'PSM-E-DRIFT' => [
|
|
177
|
+
:drift, "Some files are out of date with your bundle:\n%<list>s",
|
|
178
|
+
'Run `bundle exec proscenium install`, then commit the files it lists.'
|
|
179
|
+
],
|
|
180
|
+
'PSM-E-NOT-REGISTERED' => [
|
|
181
|
+
:drift, "%<file>s doesn't list .proscenium/packages/*, so %<manager>s won't install " \
|
|
182
|
+
"your gems' NPM dependencies.",
|
|
183
|
+
'Run `bundle exec proscenium install`, then commit %<file>s.'
|
|
184
|
+
],
|
|
185
|
+
'PSM-E-GITIGNORE' => [
|
|
186
|
+
:drift, ".gitignore is missing some of Proscenium's lines, or a later rule hides the " \
|
|
187
|
+
'dependency contexts you need to commit.',
|
|
188
|
+
'Run `bundle exec proscenium install` to add them, then commit .gitignore.'
|
|
189
|
+
],
|
|
190
|
+
'PSM-E-LOCKFILE-MISSING' => [
|
|
191
|
+
:drift, '%<lockfile>s is missing.',
|
|
192
|
+
'Run `bundle exec proscenium install`, then commit %<lockfile>s.'
|
|
193
|
+
],
|
|
194
|
+
'PSM-E-NOT-INSTALLED' => [
|
|
195
|
+
:drift, "Your NPM dependencies aren't installed: there is no node_modules " \
|
|
196
|
+
'directory.',
|
|
197
|
+
'Run `bundle exec proscenium install`, or `bundle exec proscenium install --frozen` on ' \
|
|
198
|
+
'a fresh checkout or in CI.'
|
|
199
|
+
],
|
|
200
|
+
'PSM-E-REGISTRY-TARBALL' => [
|
|
201
|
+
:integrity, 'The lockfile installs %<packages>s from a registry or Git, instead of ' \
|
|
202
|
+
'from the gem itself.',
|
|
203
|
+
'Remove the registry or GitHub dependency on it from your package.json, since ' \
|
|
204
|
+
'Gemfile.lock provides the gem now, then run `bundle exec proscenium install`.'
|
|
205
|
+
],
|
|
206
|
+
'PSM-E-WORKSPACE-MISSING' => [
|
|
207
|
+
:integrity, "%<manager>s didn't install the dependency contexts for %<gems>s.",
|
|
208
|
+
'Check that `packages` in pnpm-workspace.yaml, or `workspaces` in package.json, ' \
|
|
209
|
+
'lists ".proscenium/packages/*", then run `bundle exec proscenium install` again.'
|
|
210
|
+
],
|
|
211
|
+
'PSM-E-PEER-SPLIT' => [
|
|
212
|
+
:integrity, '%<gem>s and your app use different copies of %<package>s, so the page ' \
|
|
213
|
+
'would load it twice.',
|
|
214
|
+
"Change your app's %<package>s to a version within %<gem>s's peer range, then run " \
|
|
215
|
+
'`bundle exec proscenium install` again.'
|
|
216
|
+
],
|
|
217
|
+
'PSM-E-COLLISION' => [
|
|
218
|
+
:input, "Some of your app's dependencies use a name a gem's dependency context " \
|
|
219
|
+
"needs:\n%<list>s",
|
|
220
|
+
'Remove each one, since Gemfile.lock provides the gem now, or rename the package.'
|
|
221
|
+
],
|
|
222
|
+
'PSM-E-BUN-DEFAULT-TRUSTED' => [
|
|
223
|
+
:unsupported, "`bun pm default-trusted` didn't print Bun's default list of trusted " \
|
|
224
|
+
"packages, so Proscenium can't check whose install scripts Bun would run.",
|
|
225
|
+
'Add `"trustedDependencies": []` to package.json, listing any packages whose install ' \
|
|
226
|
+
'scripts your app needs, so Bun uses that list instead of its default.'
|
|
227
|
+
],
|
|
228
|
+
'PSM-E-TRUSTED-SOURCE' => [
|
|
229
|
+
:input, 'Some gems install a package from Git, a URL or an alias to another package, ' \
|
|
230
|
+
'under a name your package manager trusts to run install scripts, so that ' \
|
|
231
|
+
"code's install script would run:\n%<list>s",
|
|
232
|
+
"Ask the gem's author to depend on the registry package instead, or opt the gem out " \
|
|
233
|
+
'with `"proscenium": {"gemOverrides": {"<gem>": {"participate": false}}}` in ' \
|
|
234
|
+
'package.json. pnpm trusts the packages pnpm-workspace.yaml approves builds for; Bun ' \
|
|
235
|
+
'trusts trustedDependencies in package.json, or its own default list without one.'
|
|
236
|
+
],
|
|
237
|
+
'PSM-E-PLATFORM-VARIANT' => [
|
|
238
|
+
:unsupported, 'The platform-specific builds of %<variants>s differ from the one ' \
|
|
239
|
+
'installed here in their NPM dependencies or whether they opt ' \
|
|
240
|
+
"in, or don't pass `proscenium gem check`.",
|
|
241
|
+
'Make every platform build of the gem ship the same package.json dependencies and ' \
|
|
242
|
+
'proscenium.dependencies setting, each passing `proscenium gem check`. Your lockfile ' \
|
|
243
|
+
'can only hold one set.'
|
|
244
|
+
],
|
|
245
|
+
'PSM-E-UNC-SHIM' => [
|
|
246
|
+
:unsupported, "%<manager>s is installed as a .cmd script, and cmd.exe can't run in " \
|
|
247
|
+
"%<root>s because it's a network (UNC) path: it would run in " \
|
|
248
|
+
'C:\\Windows instead.',
|
|
249
|
+
'Map a drive letter to the share (`net use Z: \\\\server\\share`) and run from that ' \
|
|
250
|
+
'drive, or install %<manager>s as a native executable.'
|
|
251
|
+
],
|
|
252
|
+
'PSM-E-OWNED-LINK' => [
|
|
253
|
+
:input, '%<paths>s is a symbolic link, so installing would write outside your app.',
|
|
254
|
+
'Replace it with a real directory. Proscenium only writes inside .proscenium/.'
|
|
255
|
+
],
|
|
256
|
+
'PSM-E-OWNED-DIR' => [
|
|
257
|
+
:input, ".proscenium/packages/ contains things Proscenium didn't create: %<entries>s.",
|
|
258
|
+
'Move them somewhere else. Proscenium manages that directory, and treats everything ' \
|
|
259
|
+
'in it as a gem dependency context.'
|
|
260
|
+
],
|
|
261
|
+
'PSM-W-END-OF-LIFE' => [
|
|
262
|
+
:success, '%<manager>s %<version>s reaches end of life on %<eol>s, and a future ' \
|
|
263
|
+
'Proscenium release may stop supporting it.',
|
|
264
|
+
'Plan an upgrade to a newer %<manager>s version; the upgrade guide explains the steps.'
|
|
265
|
+
],
|
|
266
|
+
'PSM-E-INTERNAL' => [
|
|
267
|
+
:internal, 'Something went wrong inside Proscenium: %<detail>s',
|
|
268
|
+
'This is a bug in Proscenium. Please report it at ' \
|
|
269
|
+
'https://github.com/joelmoss/proscenium/issues, including the output above.'
|
|
270
|
+
]
|
|
271
|
+
}.freeze
|
|
272
|
+
|
|
273
|
+
attr_reader :code, :fix, :details
|
|
274
|
+
# What the package manager printed, for a native failure: not shown, only searched.
|
|
275
|
+
attr_accessor :output
|
|
276
|
+
|
|
277
|
+
# `escape`, when given, is the consumer's way around a gem author's problem, appended to the
|
|
278
|
+
# fix.
|
|
279
|
+
def initialize(code, details: {}, escape: nil, **args)
|
|
280
|
+
@code = code
|
|
281
|
+
status, message, fix = CATALOG.fetch(code)
|
|
282
|
+
@status = status
|
|
283
|
+
@fix = [format(fix, **args), escape].compact.join(' ')
|
|
284
|
+
@details = details
|
|
285
|
+
super(format(message, **args))
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def exit_status = EXIT.fetch(@status)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubygems/package'
|
|
4
|
+
require 'zlib'
|
|
5
|
+
require_relative '../bundled_gems'
|
|
6
|
+
require_relative 'rules'
|
|
7
|
+
|
|
8
|
+
module Proscenium
|
|
9
|
+
module CLI
|
|
10
|
+
# A built gem's specification and package.json, read from its archive without unpacking it
|
|
11
|
+
# (#154). Nothing is extracted, so no entry name can write anywhere, and the manifest entry is
|
|
12
|
+
# read only if it is a regular file within the size limit (C29).
|
|
13
|
+
module GemArchive
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# [spec, manifest text or nil, why it could not be read or nil].
|
|
17
|
+
def read(path)
|
|
18
|
+
spec = Gem::Package.new(path).spec
|
|
19
|
+
root = BundledGems.frontend_root(spec) || ''
|
|
20
|
+
wanted = root.empty? ? 'package.json' : File.join(root, 'package.json')
|
|
21
|
+
manifest = cause = nil
|
|
22
|
+
File.open(path, 'rb') do |io|
|
|
23
|
+
Gem::Package::TarReader.new(io).each do |entry|
|
|
24
|
+
next unless entry.full_name == 'data.tar.gz'
|
|
25
|
+
|
|
26
|
+
Zlib::GzipReader.wrap(entry) do |gz|
|
|
27
|
+
Gem::Package::TarReader.new(gz).each do |file|
|
|
28
|
+
manifest, cause = manifest_entry(file) if file.full_name == wanted
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
[spec, manifest, cause]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def manifest_entry(entry)
|
|
37
|
+
return [nil, 'it is not a regular file'] unless entry.file?
|
|
38
|
+
return [nil, 'it is larger than 1 MB'] if entry.header.size > Rules::MANIFEST_LIMIT
|
|
39
|
+
|
|
40
|
+
[entry.read, nil]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|