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

feat(npm): support pnpm catalogs #33376

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

fpapado
Copy link

@fpapado fpapado commented Jan 2, 2025

Changes

This PR adds support for pnpm catalogs, extracting dependencies from / updating dependencies in pnpm-workspace.yaml files.

I took @rarkins' suggestion in #30079 (comment), and added this as part of the npm manager. I tried to keep the functionality contained in pnpm files inside the relevant modules, to make it easier to split out in the future, if needed.

There's a test failing that makes assertions about the settings, but let me tell you about a couple of open questions before I go down changing those tests 😌

Open question 1: ways of picking up pnpm-workspace.yaml for extraction

I have placed pnpm-workspace.yaml in the fileMatch defaultConfig of the npm manager, and added an extra branch to extractAllPackageFiles.

An alternative would be to do this work in postExtract, by looking up sibling/parent pnpm-workspace.yaml file, and doing similar processing. This avoids the need for changing the config, but might have downsides that I cannot think of. What do you think?

Open question 2: depType and allowing granular updates to named catalogs

In the current version, I gave catalog-related upgrades a depType of pnpm.catalog, just for namespacing. However, I am wondering whether we could go more granular with this, such as pnpm.catalog.<catalogName>

One of the pnpm examples for catalogs, is allowing incremental updates, by defining named catalogs:

catalogs:
  # Can be referenced through "catalog:react17"
  react17:
    react: ^17.0.2
    react-dom: ^17.0.2

  # Can be referenced through "catalog:react18"
  react18:
    react: ^18.2.0
    react-dom: ^18.2.0

In such a scenario, I imagine users wanting to keep react 17 and 18 each in their range (allowing for bugfixes), without bumping the major. To do that, we would need to expose the catalog name to the user for matching, such as {matchDepTypes: ['pnpm.catalog.react17']}.

Have you had similar scenarios in the past? I noticed that pnpm.overrides kind of works like this, when using the pkg-a>pkg-b syntax (reference, example), but based on the depName. This could probably be done in a follow-up PR, but it's good to think about 💭

Context

Closes #30079

pnpm catalogs are described in the pnpm docs

This is my first Renovate contribution, I'm open to any and all ideas 😊

Documentation (please check one with an [x])

  • I have updated the documentation, or
  • No documentation update is required

(I believe no documentation update is required, but I'm more than happy to add anything you deem necessary ✍️)

How I've tested my work (please select one)

I have verified these changes via:

  • Code inspection only, or
  • Newly added/modified unit tests, or
  • No unit tests but ran on a real repository, or
  • Both unit tests + ran on a real repository

I added unit tests for the extraction and updates, the latter being a similar set to the package.json update tests. I will double-check the code coverage on CI, I think I missed something when running coverage locally.

I did a fair bit of repository testing at https://github.com/fpapado/renovate-testing-catalogs/pulls, to understand how the npm extraction/update phases work for package.json files.

Comment on lines +33 to +38
let document;
let parsedContents;

try {
document = parseSingleYamlDocument(fileContent);
parsedContents = pnpmCatalogsSchema.parse(document.toJS());
Copy link
Author

@fpapado fpapado Jan 2, 2025

Choose a reason for hiding this comment

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

I initially opted to do the YAML replacements with a document representation, instead of string replacements, because it seemed simpler as a start. I particularly did not want to drill down into the catalogs, accounting for the different YAML Map representations, string quoting etc 💫

Now that the tests are relatively set, I could change the implementation to do string replacement, if you think that is best.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Does your solution keep whitespaces and comments?

If yes, you can keep it like it is.

Comment on lines +147 to +150
export function parseSingleYamlDocument(
content: string,
options?: YamlParseDocumentOptions,
): Document {
Copy link
Author

Choose a reason for hiding this comment

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

I added this just to keep the parsing centralised, even if there are no other use-cases for YAML Document transforms at the moment.

@fpapado fpapado marked this pull request as ready for review January 2, 2025 17:47
@secustor secustor self-requested a review January 2, 2025 21:18
@viceice viceice self-requested a review January 4, 2025 16:11
@fpapado
Copy link
Author

fpapado commented Jan 5, 2025

Ah, I noticed now in my test repo that the lockfile does not get updated after a standalone catalog dependency upgrade. I missed that codepath when trying to keep the functionality minimal. I'll add it to my todo 🗒️

@rarkins rarkins marked this pull request as draft January 8, 2025 12:27
@fpapado
Copy link
Author

fpapado commented Jan 9, 2025

@secustor and @viceice, apologies for the ping, do you have any pointers for the open questions here? If you think the PR is not in a reviewable state, I'm happy to implement with my gut feeling and re-request a review later down the line. I'm asking because I don't want to waste your time if something ends up being unworkable or unidiomatic, but I also don't want this to be stuck in limbo 😌

@viceice
Copy link
Member

viceice commented Jan 9, 2025

we usually don't review draft PRs, will try to have a look tomorrow

Copy link
Collaborator

@secustor secustor left a comment

Choose a reason for hiding this comment

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

I had it on my plate, but haven't come around to finish it.

Open question 1: ways of picking up pnpm-workspace.yaml for extraction

pnpm-workspace.yaml should be part of fileMatch.
The expectation is that users will have use cases which deviating filenames e.g. because they are overwriting this name in a config.
Ideally we detect the fact that it is a workspace file via schema or other marker and are not relying on it. That way we are not bound to the filename and it "magically" works.

Open question 2: depType and allowing granular updates to named catalogs

Having pnpm.catalog.foo is preferable as with string pattern matching users can match all catalogs if needed.
Can you add an example of this in the readme.md that way it will show up in the manager docs.

Comment on lines +236 to +240
/**
* A pnpm catalog is either the default catalog (catalog:, catlog:default), or a
* named one (under catalogs:)
*/
type PnpmCatalog = { name: string; dependencies: NpmPackageDependency };
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be moved to ./types.ts

Comment on lines +248 to +250
let pnpmCatalogs: Array<PnpmCatalog>;
try {
pnpmCatalogs = parsePnpmCatalogs(content);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
let pnpmCatalogs: Array<PnpmCatalog>;
try {
pnpmCatalogs = parsePnpmCatalogs(content);
const pnpmCatalogs: PnpmCatalog[] = [];
try {
pnpmCatalogs.push(...parsePnpmCatalogs(content));

consistency

const extracted = extractPnpmCatalogDeps(pnpmCatalogs);

if (!extracted) {
logger.debug({ packageFile }, 'No dependencies found');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
logger.debug({ packageFile }, 'No dependencies found');

This information is already logged outside of the manager

return null;
}

logger.debug(extracted, 'Extracted catalog dependencies.');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
logger.debug(extracted, 'Extracted catalog dependencies.');

Same as above

}

function extractPnpmCatalogDeps(
catalogs: Array<PnpmCatalog>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
catalogs: Array<PnpmCatalog>,
catalogs: PnpmCatalog[],

Comment on lines +33 to +44
const expected = yamlCodeBlock`
packages:
- pkg-a

catalog:
react: 19.0.0
`;
const testContent = npmUpdater.updateDependency({
fileContent: pnpmWorkspaceYaml,
upgrade,
});
expect(testContent).toEqual(expected);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const expected = yamlCodeBlock`
packages:
- pkg-a
catalog:
react: 19.0.0
`;
const testContent = npmUpdater.updateDependency({
fileContent: pnpmWorkspaceYaml,
upgrade,
});
expect(testContent).toEqual(expected);
const testContent = npmUpdater.updateDependency({
fileContent: pnpmWorkspaceYaml,
upgrade,
});
expect(testContent).toEqual(yamlCodeBlock`
packages:
- pkg-a
catalog:
react: 19.0.0
`;);

Inline expected for better readability. Same goes for the other tests

newValue = getNewGitValue(upgrade) ?? newValue;
newValue = getNewNpmAliasValue(newValue, upgrade) ?? newValue;

logger.debug(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
logger.debug(
logger.trace(

Comment on lines +33 to +38
let document;
let parsedContents;

try {
document = parseSingleYamlDocument(fileContent);
parsedContents = pnpmCatalogsSchema.parse(document.toJS());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does your solution keep whitespaces and comments?

If yes, you can keep it like it is.

Comment on lines +28 to +31
): string | undefined {
if (!upgrade.npmPackageAlias) {
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
): string | undefined {
if (!upgrade.npmPackageAlias) {
return;
}
): string | null {
if (!upgrade.npmPackageAlias) {
return null;
}

Code base convention is to never return actively undefined

Comment on lines +4 to +7
export function getNewGitValue(upgrade: Upgrade): string | undefined {
if (!upgrade.currentRawValue) {
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export function getNewGitValue(upgrade: Upgrade): string | undefined {
if (!upgrade.currentRawValue) {
return;
}
export function getNewGitValue(upgrade: Upgrade): string | null {
if (!upgrade.currentRawValue) {
return null;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support pnpm Catalogs
3 participants