-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for centralized package manager abstraction for npm_and_yarn
ecosystem
#10862
Add support for centralized package manager abstraction for npm_and_yarn
ecosystem
#10862
Conversation
02ad7aa
to
6383a57
Compare
lockfiles: { npm: package_lock || shrinkwrap, yarn: yarn_lock, pnpm: pnpm_lock } | ||
), T.nilable(PackageManager)) | ||
sig { returns(PackageManagerHelper) } | ||
def package_manager_helper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We change the naming as package_manager_helper
to reduce confusion.
- This class was used as utility to install
package manager
forfile_fetcher
- We created Package Manager for each
npm
,yarn
andpnpm
and the helper is going to also help to create instance for package manager.
), | ||
T.nilable(Ecosystem) | ||
) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is used in dependency_snapshot to acquire information related to ecosystem, package manager and in upcoming PRs is going to be language information. That is also used for deprecation and unsupported checks however currently for npm_and_yarn they are disabled.
f.name == "pnpm-lock.yaml" | ||
end, T.nilable(Dependabot::DependencyFile)) | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To create an instance from the package manager helper, we require dependency_files
. This helper is already being used in the file_fetcher
, and to avoid duplication and maintain consistency, we're using the same helper in the file_parser
.
def self.npm_version_numeric_npm6_or_higher(lockfile) | ||
lockfile_content = T.must(lockfile.content) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the lockfile is missing content, we want to return the default version rather than triggering a Sorbet type check that causes an error.
YarnPackageManager::NAME => YarnPackageManager, | ||
PNPMPackageManager::NAME => PNPMPackageManager | ||
}.freeze | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We created a dedicated Package Manager for each package manager in npm_and_yarn
to support a centralized structure. This provides ecosystem, package manager details, as well as deprecation and unsupported version information to dependabot-common
.
return manager_name.to_s if @engines[manager_name.to_s] | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class is created to detect which package manager is going to be used by dependabot
to run update process.
sig { returns(Ecosystem::VersionManager) } | ||
def package_manager | ||
name = @package_manager_detector.detect_package_manager || DEFAULT_PACKAGE_MANAGER | ||
package_manager_by_name(name) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After detection package manager we want to return the created package manager information that can be used for metrics collection or other purposes.
version = Helpers.send(:"#{name}_version_numeric", @lockfiles[name.to_sym]) | ||
|
||
package_manager_class.new(version.to_s) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to initiate package manager by it is name and for default we are using npm
if we are unable to detect package manager name from dependency_files.
9363a35
to
b55394d
Compare
@@ -182,25 +182,36 @@ def inferred_npmrc # rubocop:disable Metrics/PerceivedComplexity | |||
|
|||
sig { returns(T.nilable(T.any(Integer, String))) } | |||
def npm_version | |||
@npm_version ||= T.let(package_manager.setup("npm"), T.nilable(T.any(Integer, String))) | |||
@npm_version ||= T.let(package_manager_helper.setup("npm"), T.nilable(T.any(Integer, String))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we have constants for npm
, pnpm
, and yarn
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I created them. Will replace constants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
solved
@@ -37,7 +37,7 @@ module Helpers | |||
# Determines the npm version depends to the feature flag | |||
# If the feature flag is enabled, we are going to use the minimum version npm 8 | |||
# Otherwise, we are going to use old versionining npm 6 | |||
sig { params(lockfile: DependencyFile).returns(Integer) } | |||
sig { params(lockfile: T.nilable(DependencyFile)).returns(Integer) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any reason why this is nilable
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, npm
is our default package manager when there’s no lockfile. This method returns the default version if the lockfile is nil
. We need it to be nilable
to ensure that if only a package.json
is present, it correctly returns the default npm version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After further review, nilable
is needed for all version checks (npm, yarn, pnpm). The package manager can be specified in package.json -> engines
or package.json -> packageManager
even without a lockfile
, and in these cases, Helper
provides the default version. Since Helper
handles version detection for all package managers, keeping lockfile
as nilable
keeps the logic unified, avoiding the need for callers to handle default versions separately.
lockfile_content = lockfile&.content | ||
|
||
return NPM_V8 if lockfile_content.nil? || lockfile_content.strip.empty? | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this be simplified some more?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need a stronger check here, including a nil
check, because JSON.parse(lockfile_content)
won’t accept a nil
value, and Sorbet will flag this as an issue. Additionally, we want to ensure the content is not an empty string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I was not clearer; I meant the conditions on lines 53 and 55 can be simplified together
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed.
@@ -85,8 +88,12 @@ def self.npm_version_numeric_npm8_or_higher(lockfile) | |||
NPM_DEFAULT_VERSION # Fallback to default npm version if parsing fails | |||
end | |||
|
|||
sig { params(yarn_lock: DependencyFile).returns(Integer) } | |||
sig { params(yarn_lock: T.nilable(DependencyFile)).returns(Integer) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any reason to introduce nilable
? I'd prefer to avoid if possible
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed nillable
from all other methods. Only npm
version checks can't be strict because when there is no lock file we set default npm and need to return the default version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After further review, nilable
is needed for all version checks (npm, yarn, pnpm). The package manager can be specified in package.json -> engines
or package.json -> packageManager
even without a lockfile
, and in these cases, Helper
provides the default version. Since Helper
handles version detection for all package managers, keeping lockfile
as nilable
keeps the logic unified, avoiding the need for callers to handle default versions separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should do another pass in a separate arch clean up or refactoring pass to eliminate as many nilables
as we can. OK to punt the discussion on nilables to that time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will take a note. Another refactor PR we can try to solve this.
@engines = package_json.fetch("engines", nil) | ||
end | ||
|
||
sig { returns(T.nilable(String)) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can this return a strongly-typed value to minimize the risk of typos?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
other then npm all others are replaced to be String
can't be nil. Also current flow doesn't allow other then npm
, yarn
, and pnpm
to return here. Since we have npm
as default in other cases then this package manager we are returning the default npm
.
cce7bcd
to
88997f5
Compare
…, aligning with centralized package manager abstraction used across ecosystems.
88997f5
to
788c271
Compare
class PackageManager | ||
ECOSYSTEM = "npm_and_yarn" | ||
MANIFEST_FILENAME = "package.json" | ||
LERNA_JSON_FILENAME = "lerna.json" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lerna should be its own package manager?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need lerna because we are using lerna as workspace management. After passing the found project into it is package manager.
sig { returns(T.nilable(Dependabot::DependencyFile)) } | ||
def package_lock | ||
@package_lock ||= T.let(dependency_files.find do |f| | ||
f.name == "package-lock.json" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these are already constants somewhere; can we replace with the actual constant references?
Also OK to do these in a separate follow up PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is done.
9754203
to
3b9ad7c
Compare
Note: We need to open another PR to refactor the versioning for |
…d_package_manager
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good @kbukum1. How we will revert in case of any failures?
@kbukum1 : Can we deploy tomorrow, not right now. |
That's works. In case if there is any issue we are going to revert the changes which are on package manager. |
…d_package_manager
…attribute in manifest file
What are you trying to accomplish?
This PR updates the
npm_and_yarn
ecosystem by introducing support for centralized package manager abstraction. Previously, the ecosystem relied onPackageManagerHelper
for managing package managers like npm, yarn, and pnpm, without distinguishing them into separate classes.With this change, we are introducing individual package manager classes (
NpmPackageManager
,YarnPackageManager
,PNPMPackageManager
) in line with the centralized package manager abstraction that is already used for other ecosystems such as Bundler and Composer. This bringsnpm_and_yarn
in line with the architecture used across different ecosystems, providing a more uniform and extensible approach.By centralizing the package manager handling for
npm_and_yarn
, we aim to standardize and simplify package manager management across ecosystems, improving code maintainability, consistency, and scalability.Anything you want to highlight for special attention from reviewers?
npm
,yarn
, andpnpm
package managers are handled by creating distinct classes for each of them.PackageManagerHelper
class is being refactored to use the new abstraction, and this change should align with the existing centralized model in use for other ecosystems.How will you know you've accomplished your goal?
npm_and_yarn
ecosystem will have distinct package manager classes fornpm
,yarn
, andpnpm
, similar to how it is done for Bundler and Composer ecosystems.npm_and_yarn
ecosystem will be centralized, making it consistent with the rest of the project.Checklist