proscenium 0.26.0-x64-mingw-ucrt
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.dll +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
data/exe/proscenium
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Run as `bundle exec proscenium`. Loads the CLI alone: never `proscenium` itself, Rails, FFI or
|
|
5
|
+
# the engine library (see lib/proscenium/cli.rb).
|
|
6
|
+
# When the CLI started loading, for the timings install reports (C40).
|
|
7
|
+
PROSCENIUM_STARTED = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
8
|
+
|
|
9
|
+
# Every file the CLI reads (package.json, contexts, pnpm-workspace.yaml) is UTF-8, whatever the
|
|
10
|
+
# locale: under LANG=C, as minimal Docker and CI images run, Ruby would read them as US-ASCII.
|
|
11
|
+
# Ruby warns of the setting, which is meant for this process alone.
|
|
12
|
+
verbose = $VERBOSE
|
|
13
|
+
$VERBOSE = nil
|
|
14
|
+
Encoding.default_external = Encoding::UTF_8
|
|
15
|
+
$VERBOSE = verbose
|
|
16
|
+
|
|
17
|
+
require 'proscenium/cli'
|
|
18
|
+
|
|
19
|
+
exit Proscenium::CLI.start(ARGV)
|
data/exe/psm
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators/base'
|
|
4
|
+
|
|
5
|
+
module Proscenium
|
|
6
|
+
module Generators
|
|
7
|
+
# Sets up `bun test` so it can import the same JS, TS, JSX and CSS the app serves.
|
|
8
|
+
#
|
|
9
|
+
# rails generate proscenium:bun
|
|
10
|
+
#
|
|
11
|
+
# Safe to run more than once: the preload is created if missing, and bunfig.toml is merged
|
|
12
|
+
# rather than overwritten, because an app may already have one with its own preloads.
|
|
13
|
+
class BunGenerator < Rails::Generators::Base
|
|
14
|
+
PRELOAD_PATH = 'test/proscenium.preload.js'
|
|
15
|
+
PRELOAD_ENTRY = './test/proscenium.preload.js'
|
|
16
|
+
BUNFIG_PATH = 'bunfig.toml'
|
|
17
|
+
|
|
18
|
+
source_root File.expand_path('templates', __dir__)
|
|
19
|
+
|
|
20
|
+
desc 'Configure bun test to run against your Proscenium-bundled JavaScript.'
|
|
21
|
+
|
|
22
|
+
def create_preload
|
|
23
|
+
template 'proscenium.preload.js', PRELOAD_PATH
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def configure_bunfig
|
|
27
|
+
return create_file(BUNFIG_PATH, default_bunfig) unless File.exist?(bunfig_full_path)
|
|
28
|
+
|
|
29
|
+
contents = File.read(bunfig_full_path)
|
|
30
|
+
|
|
31
|
+
if already_preloaded?(contents)
|
|
32
|
+
say_status :identical, BUNFIG_PATH, :blue
|
|
33
|
+
elsif (updated = with_preload(contents))
|
|
34
|
+
File.write(bunfig_full_path, updated)
|
|
35
|
+
say_status :update, BUNFIG_PATH, :green
|
|
36
|
+
else
|
|
37
|
+
say_manual_step
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def report
|
|
42
|
+
say ''
|
|
43
|
+
say 'Now write a test that imports your app code, and run `bun test`:'
|
|
44
|
+
say ''
|
|
45
|
+
say ' // test/js/button.test.jsx'
|
|
46
|
+
say ' import { expect, test } from "bun:test";'
|
|
47
|
+
say ' import Button from "/app/components/button.jsx";'
|
|
48
|
+
say ''
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Only a real entry counts. The bare string can also appear in a commented-out line, and
|
|
54
|
+
# reporting "identical" then would silently do nothing for someone who had disabled it.
|
|
55
|
+
def already_preloaded?(contents)
|
|
56
|
+
test_table(contents).to_s.match?(/^[^#\n]*["']#{Regexp.escape(PRELOAD_ENTRY)}["']/o)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns the whole file with our entry added inside the `[test]` table, or nil when that
|
|
60
|
+
# cannot be done without risking the file.
|
|
61
|
+
#
|
|
62
|
+
# Editing TOML with a regex is how this went wrong before, so the rules are narrow and each
|
|
63
|
+
# refusal falls through to printing the two lines for the user to add:
|
|
64
|
+
#
|
|
65
|
+
# - a `preload` key outside `[test]` is left alone. It belongs to `bun run`, and appending
|
|
66
|
+
# to it put the test preload somewhere `bun test` never reads.
|
|
67
|
+
# - `[test]` already present without a `preload` key gets the key inserted into it. The
|
|
68
|
+
# previous version appended a second `[test]` table, which is invalid TOML.
|
|
69
|
+
# - an existing `[test] preload` array is extended in place, respecting a trailing comma.
|
|
70
|
+
# Appending `, "entry"` after one produced `,\n, "entry"`, also invalid TOML.
|
|
71
|
+
def with_preload(contents)
|
|
72
|
+
table = test_table(contents)
|
|
73
|
+
|
|
74
|
+
return "#{contents.sub(/\n*\z/, "\n")}\n#{default_bunfig}" if table.nil?
|
|
75
|
+
|
|
76
|
+
if (array = table[/^[^#\n]*\bpreload\s*=\s*\[.*?\]/m])
|
|
77
|
+
extended = extend_array(array)
|
|
78
|
+
return nil if extended.nil?
|
|
79
|
+
|
|
80
|
+
# Substituted inside the table, then the table inside the document. `contents.sub(array)`
|
|
81
|
+
# replaces the FIRST occurrence of that text anywhere - so a root-level `preload` holding
|
|
82
|
+
# the same entries as `[test]`'s got edited instead, which put the harness in `bun run`,
|
|
83
|
+
# left `bun test` without it, and reported success.
|
|
84
|
+
return contents.sub(table) { table.sub(array) { extended } }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# `[test]` exists but has no preload key: put one directly under its header.
|
|
88
|
+
header = table[/\A\[test\][^\n]*\n/]
|
|
89
|
+
return nil if header.nil?
|
|
90
|
+
|
|
91
|
+
contents.sub(table) { table.sub(header, %(#{header}preload = ["#{PRELOAD_ENTRY}"]\n)) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The `[test]` table's text, from its header to the next table header or end of file. Nil
|
|
95
|
+
# when the file has no `[test]` table.
|
|
96
|
+
# The header line may end at EOF rather than a newline. Requiring the newline made a bunfig
|
|
97
|
+
# whose last line is `[test]` look like it had no `[test]` table at all, and the nil branch
|
|
98
|
+
# above then appended a second one - the invalid-TOML shape this rewrite exists to avoid.
|
|
99
|
+
# Matching it here instead leaves `with_preload` unable to find a header to insert under, so
|
|
100
|
+
# it refuses and prints the two lines for the user to add.
|
|
101
|
+
def test_table(contents)
|
|
102
|
+
contents[/^\[test\][^\n]*(?:\n|\z).*?(?=^\[|\z)/m]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Adds our entry to an existing array, keeping the file's own shape. A trailing comma is
|
|
106
|
+
# respected rather than doubled - appending ", entry" after one is what produced invalid
|
|
107
|
+
# TOML before. Returns nil when the array's shape is one this cannot edit safely.
|
|
108
|
+
def extend_array(array)
|
|
109
|
+
inner = array[/\[(.*)\]/m, 1]
|
|
110
|
+
entry = %("#{PRELOAD_ENTRY}")
|
|
111
|
+
|
|
112
|
+
return array.sub(/\[\s*\]/m, "[#{entry}]") if inner.strip.empty?
|
|
113
|
+
|
|
114
|
+
# A comment after the last entry swallows the separator: the inserted `, ` lands inside
|
|
115
|
+
# `# DOM shim` and the two entries end up with no comma between them - valid input, invalid
|
|
116
|
+
# output. Putting the comma before the comment means parsing TOML comments, which is the
|
|
117
|
+
# approximation this file keeps getting punished for. Refuse and let the user do it.
|
|
118
|
+
# A comment after the last entry swallows the separator: the inserted `, ` lands inside
|
|
119
|
+
# `# DOM shim` and the two entries end up with no comma between them - valid input, invalid
|
|
120
|
+
# output. Putting the comma before the comment means parsing TOML comments, which is the
|
|
121
|
+
# approximation this file keeps getting punished for. Refuse and let the user do it.
|
|
122
|
+
return nil if inner.match?(/(?<!["'])#/)
|
|
123
|
+
|
|
124
|
+
separator = inner.rstrip.end_with?(',') ? '' : ', '
|
|
125
|
+
array.sub(/(\s*)\]\z/) { "#{separator}#{::Regexp.last_match(1)}#{entry}]" }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def say_manual_step
|
|
129
|
+
say_status :skip, BUNFIG_PATH, :yellow
|
|
130
|
+
say ''
|
|
131
|
+
say " Could not edit #{BUNFIG_PATH} safely. Add this to it by hand:"
|
|
132
|
+
say ''
|
|
133
|
+
say ' [test]'
|
|
134
|
+
say %( preload = ["#{PRELOAD_ENTRY}"])
|
|
135
|
+
say ''
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def bunfig_full_path
|
|
139
|
+
File.expand_path(BUNFIG_PATH, destination_root)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def default_bunfig
|
|
143
|
+
<<~TOML
|
|
144
|
+
[test]
|
|
145
|
+
preload = ["#{PRELOAD_ENTRY}"]
|
|
146
|
+
TOML
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Registers Proscenium's Bun plugin, so `bun test` can import the same JS, TS, JSX and CSS your
|
|
2
|
+
// Rails app serves. Generated by `rails generate proscenium:bun`.
|
|
3
|
+
//
|
|
4
|
+
// Everything of substance lives in the gem, so this file should not need to change when
|
|
5
|
+
// Proscenium does.
|
|
6
|
+
//
|
|
7
|
+
// Keep this LAST in bunfig.toml's `preload` list. The plugin claims every uncached JS module, so
|
|
8
|
+
// anything that has to come from node_modules - a DOM shim, a testing library - must already be
|
|
9
|
+
// imported and cached by an earlier preload, or it gets fetched from your Rails app instead.
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, statSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
// Written to a file rather than read off a pipe, because a pipe cannot be relied on from here.
|
|
15
|
+
// Once happy-dom's GlobalRegistrator has run and a DOM-touching package has been imported - which
|
|
16
|
+
// is the normal state of a preload that comes before this one - every `Bun.spawn`/`Bun.spawnSync`
|
|
17
|
+
// in the process returns a zero-length stdout: exit status 0, empty stderr, no bytes, for any
|
|
18
|
+
// command at all.
|
|
19
|
+
//
|
|
20
|
+
// The file goes in a private directory under the app's own tmp/, beside where the daemon
|
|
21
|
+
// materialises `.rjs` modules, rather than in the shared system temp dir. `mkdtemp` is what makes
|
|
22
|
+
// it safe either way - the name stops being guessable and the create is exclusive by construction,
|
|
23
|
+
// so nobody else on the machine can pre-place a file here and break or redirect the run - but an
|
|
24
|
+
// app-private parent means there is no one else in the directory to begin with.
|
|
25
|
+
mkdirSync("tmp/proscenium", { recursive: true });
|
|
26
|
+
const gemDirDir = mkdtempSync(join("tmp/proscenium", "gem-dir-"));
|
|
27
|
+
const gemDirFile = join(gemDirDir, "gem-dir");
|
|
28
|
+
|
|
29
|
+
let gemDir = "";
|
|
30
|
+
try {
|
|
31
|
+
Bun.spawnSync(["bundle", "exec", "ruby", "-e", "print Gem.loaded_specs['proscenium'].gem_dir"], {
|
|
32
|
+
stdout: Bun.file(gemDirFile),
|
|
33
|
+
stderr: "inherit",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
gemDir = (await Bun.file(gemDirFile).text()).trim();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// `Bun.spawnSync` throws rather than returning when the command cannot be run at all - no
|
|
39
|
+
// bundler on PATH, or not a Ruby project - and then the output file was never created, so the
|
|
40
|
+
// read throws too. Both land here so the message below is the one thing the developer sees.
|
|
41
|
+
gemDir = "";
|
|
42
|
+
if (error?.code !== "ENOENT") throw error;
|
|
43
|
+
} finally {
|
|
44
|
+
rmSync(gemDirDir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Checked for being a directory, not merely non-empty: bundler writes some of its own
|
|
48
|
+
// diagnostics to stdout ("Could not locate Gemfile", for one), which would otherwise sail past a
|
|
49
|
+
// truthiness check and fail later as an unreadable module path.
|
|
50
|
+
if (!gemDir || !existsSync(gemDir) || !statSync(gemDir).isDirectory()) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"could not locate the proscenium gem. `bundle exec ruby -e \"print " +
|
|
53
|
+
"Gem.loaded_specs['proscenium'].gem_dir\"` did not name a directory" +
|
|
54
|
+
(gemDir ? ` - it produced ${JSON.stringify(gemDir.slice(0, 200))}` : " - it produced nothing") +
|
|
55
|
+
". Is this a Rails app with proscenium in its Gemfile, and is bundler on PATH?",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const { register } = await import(`${gemDir}/lib/proscenium/runtime/bootstrap.js`);
|
|
60
|
+
|
|
61
|
+
await register();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Proscenium
|
|
6
|
+
# What the app's own package.json says that the context map needs (#154). Like ContextMap, which
|
|
7
|
+
# it reads JSON through, it loads nothing the `proscenium` CLI may not.
|
|
8
|
+
module AppPackage
|
|
9
|
+
LOCAL_SPECS = %w[link: file: workspace: portal:].freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# The names of the app's own `link:`, `file:` and workspace dependencies, peers included, as
|
|
14
|
+
# pnpm installs those too, and in a Bun app any workspace member it depends on by version,
|
|
15
|
+
# which Bun links. They keep their link paths once dependency contexts make other packages
|
|
16
|
+
# real-path.
|
|
17
|
+
def local_packages(root)
|
|
18
|
+
package = read(root) or return []
|
|
19
|
+
|
|
20
|
+
members = bun?(root, package) ? workspace_members(root, package['workspaces']) : []
|
|
21
|
+
%w[dependencies devDependencies optionalDependencies peerDependencies].flat_map do |field|
|
|
22
|
+
deps = package[field]
|
|
23
|
+
next [] unless deps.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
deps.select { |name, spec| spec.to_s.start_with?(*LOCAL_SPECS) || members.include?(name) }
|
|
26
|
+
.keys
|
|
27
|
+
end.uniq.sort
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Whether Bun installs the app: packageManager names it, or else its lockfile is there.
|
|
31
|
+
def bun?(root, package)
|
|
32
|
+
named = package['packageManager']
|
|
33
|
+
return named.split('@').first == 'bun' if named.is_a?(String)
|
|
34
|
+
|
|
35
|
+
%w[bun.lock bun.lockb].any? { File.exist?(File.join(root, it)) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The names of the packages `workspaces` (an array, or an object's `packages`) takes in, less
|
|
39
|
+
# those a `!` pattern leaves out, read as Bun's glob does: braces and `**` included.
|
|
40
|
+
def workspace_members(root, workspaces)
|
|
41
|
+
patterns = Array(workspaces.is_a?(Hash) ? workspaces['packages'] : workspaces).grep(String)
|
|
42
|
+
excluded, included = patterns.map { it.delete_prefix('./').chomp('/') }
|
|
43
|
+
.partition { it.start_with?('!') }
|
|
44
|
+
dirs = included.flat_map { Dir.glob(it, File::FNM_EXTGLOB, base: root) }.uniq
|
|
45
|
+
dirs.reject { |dir| excluded.any? { File.fnmatch?(it[1..], dir, File::FNM_EXTGLOB) } }
|
|
46
|
+
.filter_map { read(File.join(root, it))&.dig('name') }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Whether package.json's text, for a file JSON.parse refuses, registers the contexts in its
|
|
50
|
+
# `workspaces`, decoding JSON's `\/` and `\uXXXX`
|
|
51
|
+
# escapes in the key and the pattern, as Bun does.
|
|
52
|
+
def workspaces?(text) = unescape(text).match?(WORKSPACES)
|
|
53
|
+
|
|
54
|
+
# JSON text with its `\uXXXX` and `\/` escapes decoded, as Bun reads them, for matching a file
|
|
55
|
+
# JSON.parse refuses.
|
|
56
|
+
# A surrogate pair is one character; a lone half, which JSON allows, becomes U+FFFD rather
|
|
57
|
+
# than raising.
|
|
58
|
+
def unescape(text)
|
|
59
|
+
text.gsub(ESCAPE) do
|
|
60
|
+
high, low, single = ::Regexp.last_match.captures
|
|
61
|
+
if high then (0x10000 + ((high.hex - 0xD800) << 10) + (low.hex - 0xDC00)).chr(Encoding::UTF_8)
|
|
62
|
+
elsif single then SURROGATES.cover?(single.hex) ? "\uFFFD" : single.hex.chr(Encoding::UTF_8)
|
|
63
|
+
else '/'
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
ESCAPE = %r{\\u([dD][89abAB]\h{2})\\u([dD][c-fC-F]\h{2})|\\u(\h{4})|\\/}
|
|
69
|
+
SURROGATES = (0xD800..0xDFFF)
|
|
70
|
+
|
|
71
|
+
# The pattern inside package.json's `workspaces`, an array or an object's `packages`.
|
|
72
|
+
WORKSPACES = /"workspaces"\s*:\s*(?:\{[^}]*"packages"\s*:\s*)?\[[^\]]*
|
|
73
|
+
"\.proscenium\/packages\/\*"/x
|
|
74
|
+
|
|
75
|
+
# The manager `packageManager` names, or nil.
|
|
76
|
+
def package_manager(root)
|
|
77
|
+
field = read(root)&.dig('packageManager')
|
|
78
|
+
field.split('@').first if field.is_a?(String)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The app's package.json as an object, or nil when it is missing or not one.
|
|
82
|
+
def read(root)
|
|
83
|
+
path = File.join(root, 'package.json')
|
|
84
|
+
package = File.exist?(path) && ContextMap.read_json(path)
|
|
85
|
+
package if package.is_a?(Hash)
|
|
86
|
+
rescue JSON::ParserError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|