-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
base: main
Are you sure you want to change the base?
feat(npm): support pnpm catalogs #33376
Conversation
This keeps YAML parsing centralised, while allowing use of the Document represenation, in addition to the JS one.
let document; | ||
let parsedContents; | ||
|
||
try { | ||
document = parseSingleYamlDocument(fileContent); | ||
parsedContents = pnpmCatalogsSchema.parse(document.toJS()); |
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 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.
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.
Does your solution keep whitespaces and comments?
If yes, you can keep it like it is.
export function parseSingleYamlDocument( | ||
content: string, | ||
options?: YamlParseDocumentOptions, | ||
): Document { |
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 added this just to keep the parsing centralised, even if there are no other use-cases for YAML Document transforms at the moment.
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 🗒️ |
@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 😌 |
we usually don't review draft PRs, will try to have a look tomorrow |
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 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.
/** | ||
* A pnpm catalog is either the default catalog (catalog:, catlog:default), or a | ||
* named one (under catalogs:) | ||
*/ | ||
type PnpmCatalog = { name: string; dependencies: NpmPackageDependency }; |
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 should be moved to ./types.ts
let pnpmCatalogs: Array<PnpmCatalog>; | ||
try { | ||
pnpmCatalogs = parsePnpmCatalogs(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.
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'); |
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.
logger.debug({ packageFile }, 'No dependencies found'); |
This information is already logged outside of the manager
return null; | ||
} | ||
|
||
logger.debug(extracted, 'Extracted catalog dependencies.'); |
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.
logger.debug(extracted, 'Extracted catalog dependencies.'); |
Same as above
} | ||
|
||
function extractPnpmCatalogDeps( | ||
catalogs: Array<PnpmCatalog>, |
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.
catalogs: Array<PnpmCatalog>, | |
catalogs: PnpmCatalog[], |
const expected = yamlCodeBlock` | ||
packages: | ||
- pkg-a | ||
|
||
catalog: | ||
react: 19.0.0 | ||
`; | ||
const testContent = npmUpdater.updateDependency({ | ||
fileContent: pnpmWorkspaceYaml, | ||
upgrade, | ||
}); | ||
expect(testContent).toEqual(expected); |
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.
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( |
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.
logger.debug( | |
logger.trace( |
let document; | ||
let parsedContents; | ||
|
||
try { | ||
document = parseSingleYamlDocument(fileContent); | ||
parsedContents = pnpmCatalogsSchema.parse(document.toJS()); |
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.
Does your solution keep whitespaces and comments?
If yes, you can keep it like it is.
): string | undefined { | ||
if (!upgrade.npmPackageAlias) { | ||
return; | ||
} |
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.
): string | undefined { | |
if (!upgrade.npmPackageAlias) { | |
return; | |
} | |
): string | null { | |
if (!upgrade.npmPackageAlias) { | |
return null; | |
} |
Code base convention is to never return actively undefined
export function getNewGitValue(upgrade: Upgrade): string | undefined { | ||
if (!upgrade.currentRawValue) { | ||
return; | ||
} |
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.
export function getNewGitValue(upgrade: Upgrade): string | undefined { | |
if (!upgrade.currentRawValue) { | |
return; | |
} | |
export function getNewGitValue(upgrade: Upgrade): string | null { | |
if (!upgrade.currentRawValue) { | |
return null; | |
} |
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 inpnpm
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 extractionI have placed
pnpm-workspace.yaml
in thefileMatch
defaultConfig of the npm manager, and added an extra branch toextractAllPackageFiles
.An alternative would be to do this work in
postExtract
, by looking up sibling/parentpnpm-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 catalogsIn the current version, I gave catalog-related upgrades a
depType
ofpnpm.catalog
, just for namespacing. However, I am wondering whether we could go more granular with this, such aspnpm.catalog.<catalogName>
One of the pnpm examples for catalogs, is allowing incremental updates, by defining named catalogs:
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 thepkg-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 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:
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.