Module: GeneratorMessages::PackageManagerDetection
- Included in:
- GeneratorMessages
- Defined in:
- lib/generators/react_on_rails/generator_messages/package_manager_detection.rb
Overview
Package-manager detection helpers used by the install generator and the post-install message. Split out of GeneratorMessages to keep that class under Metrics/ClassLength and to group related logic together.
Constant Summary collapse
- SUPPORTED_PACKAGE_MANAGERS =
%w[npm pnpm yarn bun].freeze
- LOCKFILE_CANDIDATES_BY_MANAGER =
Hash insertion order is the detection priority used by detect_package_manager_from_lockfiles (yarn → pnpm → bun → npm).
{ "yarn" => ["yarn.lock"], "pnpm" => ["pnpm-lock.yaml"], "bun" => ["bun.lock", "bun.lockb"], "npm" => ["package-lock.json"] }.freeze
Instance Method Summary collapse
-
#detect_package_manager(app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET) ⇒ Object
Detects the package manager in priority order: 1.
- #detect_package_manager_from_lockfiles(app_root: Dir.pwd) ⇒ Object
-
#detect_package_manager_with_source(app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET) ⇒ Object
source is one of :env, :package_json, :lockfile, :default — used to name the originating source when surfacing detection errors.
- #lockfile_filename_for(package_manager, app_root: Dir.pwd) ⇒ Object
-
#lockfile_for_manager?(package_manager, app_root: Dir.pwd) ⇒ Boolean
Used by the CI scaffold so ‘cache:` / `<pm> install` never reference a lockfile that’s not on disk (e.g. ‘packageManager: pnpm` without `pnpm-lock.yaml`, which breaks `actions/setup-node`’s cache step).
-
#package_manager_declared?(manager:, app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET) ⇒ Boolean
Returns true when package.json declares a top-level ‘packageManager` field with an npm-style version/range/tag (e.g. `“pnpm@9.0.0”`, `“pnpm@^10.0.0”`, or `“pnpm@latest”`) for the requested `manager`.
- #package_manager_executable_available?(package_manager) ⇒ Boolean
-
#read_package_json(app_root) ⇒ Object
Parses package.json once and returns the hash, or nil if the file is missing or unreadable.
- #supported_package_manager?(package_manager) ⇒ Boolean
Instance Method Details
#detect_package_manager(app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET) ⇒ Object
Detects the package manager in priority order:
-
REACT_ON_RAILS_PACKAGE_MANAGER env variable
-
packageManager field in package.json (Corepack standard)
-
Lockfile on disk
-
Falls back to “npm” (Shakapacker 8.x default)
Pass app_root: to resolve paths against a specific directory (e.g. destination_root in generators) instead of Dir.pwd. Omit ‘package_json:` (the default) to read package.json from disk. Pass package_json: <parsed_hash> to reuse an already-parsed package.json and avoid a re-read (callers that also inspect scripts/deps should parse once and pass the hash). Pass package_json: nil when the caller already attempted to read package.json and wants detection to fall through directly to lockfile heuristics.
38 39 40 41 42 43 |
# File 'lib/generators/react_on_rails/generator_messages/package_manager_detection.rb', line 38 def detect_package_manager(app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET) detect_package_manager_with_source( app_root: app_root, package_json: package_json ).first end |
#detect_package_manager_from_lockfiles(app_root: Dir.pwd) ⇒ Object
105 106 107 108 109 |
# File 'lib/generators/react_on_rails/generator_messages/package_manager_detection.rb', line 105 def detect_package_manager_from_lockfiles(app_root: Dir.pwd) LOCKFILE_CANDIDATES_BY_MANAGER.keys.find do |pm| lockfile_for_manager?(pm, app_root: app_root) end end |
#detect_package_manager_with_source(app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET) ⇒ Object
source is one of :env, :package_json, :lockfile, :default — used to name the originating source when surfacing detection errors.
See ‘detect_package_manager` for the `package_json:` three-way semantics (omitted = read from disk, nil = caller cached absent, Hash = pre-parsed).
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/generators/react_on_rails/generator_messages/package_manager_detection.rb', line 50 def detect_package_manager_with_source(app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET) env_package_manager = ENV.fetch("REACT_ON_RAILS_PACKAGE_MANAGER", nil)&.strip&.downcase return [env_package_manager, :env] if supported_package_manager?(env_package_manager) content = package_json_content( app_root: app_root, package_json: package_json ) pm_from_json = content ? package_manager_name_from_content(content) : nil return [pm_from_json, :package_json] if pm_from_json pm_from_lockfile = detect_package_manager_from_lockfiles(app_root: app_root) return [pm_from_lockfile, :lockfile] if pm_from_lockfile ["npm", :default] end |
#lockfile_filename_for(package_manager, app_root: Dir.pwd) ⇒ Object
67 68 69 70 71 |
# File 'lib/generators/react_on_rails/generator_messages/package_manager_detection.rb', line 67 def lockfile_filename_for(package_manager, app_root: Dir.pwd) LOCKFILE_CANDIDATES_BY_MANAGER[package_manager]&.find do |name| File.exist?(File.join(app_root, name)) end end |
#lockfile_for_manager?(package_manager, app_root: Dir.pwd) ⇒ Boolean
Used by the CI scaffold so ‘cache:` / `<pm> install` never reference a lockfile that’s not on disk (e.g. ‘packageManager: pnpm` without `pnpm-lock.yaml`, which breaks `actions/setup-node`’s cache step).
101 102 103 |
# File 'lib/generators/react_on_rails/generator_messages/package_manager_detection.rb', line 101 def lockfile_for_manager?(package_manager, app_root: Dir.pwd) !lockfile_filename_for(package_manager, app_root: app_root).nil? end |
#package_manager_declared?(manager:, app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET) ⇒ Boolean
Returns true when package.json declares a top-level ‘packageManager` field with an npm-style version/range/tag (e.g. `“pnpm@9.0.0”`, `“pnpm@^10.0.0”`, or `“pnpm@latest”`) for the requested `manager`. The CI scaffold treats these as declared so it does not inject a conflicting fallback `version:`. Projects that need reproducible Corepack behavior should prefer an exact version, optionally with a hash (e.g. `“pnpm@9.0.0+sha256.abc”`). A bare name without `@<version>` returns false because `pnpm/action-setup` has no version to resolve from it. Used by the CI scaffold to decide whether `pnpm/action-setup` needs an explicit `version:` key; exact SemVer validation belongs only where a caller needs to extract a reproducible version pin. Pass package_json: <parsed_hash> to reuse an already-parsed package.json and package_json: nil to preserve a cached missing/unreadable read.
85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/generators/react_on_rails/generator_messages/package_manager_detection.rb', line 85 def package_manager_declared?(manager:, app_root: Dir.pwd, package_json: PACKAGE_JSON_UNSET) content = package_json_content( app_root: app_root, package_json: package_json ) return false unless content declared = versioned_package_manager_name_from_content(content) return false if declared.nil? declared == manager.to_s.downcase end |
#package_manager_executable_available?(package_manager) ⇒ Boolean
115 116 117 118 119 |
# File 'lib/generators/react_on_rails/generator_messages/package_manager_detection.rb', line 115 def package_manager_executable_available?(package_manager) return false unless supported_package_manager?(package_manager) ReactOnRails::Utils.command_available?(package_manager) end |
#read_package_json(app_root) ⇒ Object
Parses package.json once and returns the hash, or nil if the file is missing or unreadable. Generator code can reuse the same parsed hash across setup, template, and message paths.
Intentionally public: install_generator and other generator callers read package.json once and pass the result to detect_package_manager / package_manager_declared? to avoid repeated disk reads. When this returns nil, pass package_json: nil to those helpers to preserve that cached missing/unreadable state.
132 133 134 135 136 137 138 139 |
# File 'lib/generators/react_on_rails/generator_messages/package_manager_detection.rb', line 132 def read_package_json(app_root) package_json_path = File.join(app_root, "package.json") return nil unless File.exist?(package_json_path) JSON.parse(File.read(package_json_path)) rescue JSON::ParserError, Errno::EACCES, Errno::ENOENT nil end |
#supported_package_manager?(package_manager) ⇒ Boolean
111 112 113 |
# File 'lib/generators/react_on_rails/generator_messages/package_manager_detection.rb', line 111 def supported_package_manager?(package_manager) SUPPORTED_PACKAGE_MANAGERS.include?(package_manager) end |