Skip to content
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

Merged

Conversation

kbukum1
Copy link
Contributor

@kbukum1 kbukum1 commented Oct 28, 2024

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 on PackageManagerHelper 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 brings npm_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?

  • We are refactoring how npm, yarn, and pnpm package managers are handled by creating distinct classes for each of them.
  • The 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.
  • Focus should be on ensuring that this change integrates well with the existing framework and doesn't introduce regressions, particularly around how package managers are identified and utilized.

How will you know you've accomplished your goal?

  • The npm_and_yarn ecosystem will have distinct package manager classes for npm, yarn, and pnpm, similar to how it is done for Bundler and Composer ecosystems.
  • All package manager handling for the npm_and_yarn ecosystem will be centralized, making it consistent with the rest of the project.
  • The refactored code passes all relevant tests, and functionality remains consistent across the npm, yarn, and pnpm package managers.

Checklist

  • I have run the complete test suite to ensure all tests and linters pass.
  • I have thoroughly tested my code changes to ensure they work as expected, including adding additional tests for new functionality.
  • I have written clear and descriptive commit messages.
  • I have provided a detailed description of the changes in the pull request, including the problem it addresses, how it fixes the problem, and any relevant details about the implementation.
  • I have ensured that the code is well-documented and easy to understand.

@github-actions github-actions bot added L: php:composer Issues and code for Composer L: ruby:bundler RubyGems via bundler L: javascript labels Oct 28, 2024
@kbukum1 kbukum1 changed the base branch from main to kamil/update_package_manager_abstraction_for_bundler_and_composer October 28, 2024 23:07
@kbukum1 kbukum1 force-pushed the kamil/update_npm_and_yarn_support_centralized_package_manager branch from 02ad7aa to 6383a57 Compare October 29, 2024 22:49
@kbukum1 kbukum1 marked this pull request as ready for review October 30, 2024 17:10
@kbukum1 kbukum1 requested a review from a team as a code owner October 30, 2024 17:10
lockfiles: { npm: package_lock || shrinkwrap, yarn: yarn_lock, pnpm: pnpm_lock }
), T.nilable(PackageManager))
sig { returns(PackageManagerHelper) }
def package_manager_helper
Copy link
Contributor Author

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 for file_fetcher
  • We created Package Manager for each npm, yarn and pnpm and the helper is going to also help to create instance for package manager.

),
T.nilable(Ecosystem)
)
end
Copy link
Contributor Author

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

Copy link
Contributor Author

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)
Copy link
Contributor Author

@kbukum1 kbukum1 Oct 30, 2024

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

Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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.

Base automatically changed from kamil/update_package_manager_abstraction_for_bundler_and_composer to main October 31, 2024 21:07
@kbukum1 kbukum1 force-pushed the kamil/update_npm_and_yarn_support_centralized_package_manager branch from 9363a35 to b55394d Compare October 31, 2024 21:12
@@ -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)))
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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) }
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Contributor Author

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) }
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

@kbukum1 kbukum1 Nov 4, 2024

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)) }
Copy link
Member

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?

Copy link
Contributor Author

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.

@kbukum1 kbukum1 removed L: php:composer Issues and code for Composer L: ruby:bundler RubyGems via bundler labels Oct 31, 2024
@kbukum1 kbukum1 force-pushed the kamil/update_npm_and_yarn_support_centralized_package_manager branch 2 times, most recently from cce7bcd to 88997f5 Compare October 31, 2024 23:47
…, aligning with centralized package manager abstraction used across ecosystems.
@kbukum1 kbukum1 force-pushed the kamil/update_npm_and_yarn_support_centralized_package_manager branch from 88997f5 to 788c271 Compare October 31, 2024 23:50
class PackageManager
ECOSYSTEM = "npm_and_yarn"
MANIFEST_FILENAME = "package.json"
LERNA_JSON_FILENAME = "lerna.json"
Copy link
Member

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?

Copy link
Contributor Author

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"
Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done.

@kbukum1 kbukum1 force-pushed the kamil/update_npm_and_yarn_support_centralized_package_manager branch from 9754203 to 3b9ad7c Compare November 4, 2024 17:12
@kbukum1
Copy link
Contributor Author

kbukum1 commented Nov 4, 2024

Note: We need to open another PR to refactor the versioning for npm_and_yarn. Currently for file_fetcher and file_parser we are using different versioning so we need to use same version for each process. Also for following commen we need refactoring to possibly remove nilable from the sorbet typings.

Copy link
Contributor

@honeyankit honeyankit left a 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?

@honeyankit
Copy link
Contributor

@kbukum1 : Can we deploy tomorrow, not right now.

@kbukum1
Copy link
Contributor Author

kbukum1 commented Nov 5, 2024

@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.

@kbukum1 kbukum1 merged commit 2cb56bd into main Nov 5, 2024
86 checks passed
@kbukum1 kbukum1 deleted the kamil/update_npm_and_yarn_support_centralized_package_manager branch November 5, 2024 20:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants