Skip to content

feat: implement catalog protocol for install#8221

Merged
zkochan merged 7 commits intocatalogsfrom
gluxon/catalog-protocol-install
Jun 26, 2024
Merged

feat: implement catalog protocol for install#8221
zkochan merged 7 commits intocatalogsfrom
gluxon/catalog-protocol-install

Conversation

@gluxon
Copy link
Member

@gluxon gluxon commented Jun 17, 2024

Changes

This implements the following items from the tracking issue #7072.

  • Support the catalog: and catalog:default specifiers.
  • Support catalog:<name> specifiers.
  • Changing a catalog entry in pnpm-workspace.yaml causes the lock file to not be up to date.

I've organized this PR into different commits that make sense by themselves. This PR should be easier to review commit by commit rather than all files changed at once.

When should we transform the catalog protocol?

The hardest part of this PR was determining the right place for the catalog:^1.2.3 translation internally.

  • We could model this as a read package hook, which would be similar to how pnpm.overrides and packageExtensions work. I think this is too early of a place for the protocol to be resolved since it means the pnpm-lock.yaml file won't have the original catalog: specifier that was declared in package.json.
  • We could rewrite it at the @pnpm/default-resolver level, but I think that's too deep. It means the default resolver would need to understand catalogs, which leaks the catalogs abstraction and makes the resolvers more complicated.

I settled on performing the catalog: protocol replacement right before we request dependencies from the store. I think that strikes the right balance.

if (catalogLookup != null) {
wantedDependency.pref = catalogLookup.entrySpecifier
}
if (!options.update && currentPkg.version && currentPkg.pkgId?.endsWith(`@${currentPkg.version}`)) {
wantedDependency.pref = replaceVersionInPref(wantedDependency.pref, currentPkg.version)
}
pkgResponse = await ctx.storeController.requestPackage(wantedDependency, {

Edit: We pivoted to resolveDependenciesOfImporters (which is still somewhere in the middle) after the discussion below. #8221 (comment)

Changes from Previous PR

This PR was previously open at #8020. A few things have changed since the last PR.

@gluxon gluxon marked this pull request as ready for review June 17, 2024 19:36
@gluxon gluxon requested a review from zkochan as a code owner June 17, 2024 19:36
@zkochan
Copy link
Member

zkochan commented Jun 17, 2024

I settled on performing the catalog: protocol replacement right before we request dependencies from the store. I think that strikes the right balance.

I am not sure about this approach. That code is executed for every single resolved package, while catalogs are only applied to the projects themselves (not to the dependencies). So, maybe it should happen somewhere here

const resolveArgs: ImporterToResolve[] = importers.map((importer) => {
const projectSnapshot = opts.wantedLockfile.importers[importer.id]
// This may be optimized.
// We only need to proceed resolving every dependency
// if the newly added dependency has peer dependencies.
const proceed = importer.id === '.' || importer.hasRemovedDependencies === true || importer.wantedDependencies.some((wantedDep: any) => wantedDep.isNew) // eslint-disable-line @typescript-eslint/no-explicit-any
const resolveOpts: ImporterToResolveOptions = {
currentDepth: 0,
parentPkg: {
installable: true,
nodeId: importer.id as unknown as NodeId,
optional: false,
pkgId: importer.id as unknown as PkgResolutionId,
rootDir: importer.rootDir,
},
parentIds: [importer.id as unknown as PkgResolutionId],
proceed,
resolvedDependencies: {
...projectSnapshot.dependencies,
...projectSnapshot.devDependencies,
...projectSnapshot.optionalDependencies,
},
updateDepth: -1,
updateMatching: importer.updateMatching,
prefix: importer.rootDir,
supportedArchitectures: opts.supportedArchitectures,
updateToLatest: opts.updateToLatest,
}
return {
updatePackageManifest: importer.updatePackageManifest,
parentPkgAliases: Object.fromEntries(
importer.wantedDependencies.filter(({ alias }) => alias).map(({ alias }) => [alias, true])
) as ParentPkgAliases,
preferredVersions: importer.preferredVersions ?? {},
wantedDependencies: importer.wantedDependencies,
options: resolveOpts,
}
})

Or in the resolveRootDependencies function, which won't be executed on dependencies.

@gluxon gluxon force-pushed the gluxon/catalog-protocol-install branch 3 times, most recently from 8fc37e4 to 4bf157f Compare June 17, 2024 20:12
@gluxon
Copy link
Member Author

gluxon commented Jun 17, 2024

I am not sure about this approach. That code is executed for every single resolved package, while catalogs are only applied to the projects themselves (not to the dependencies). So, maybe it should happen somewhere here

I was also leaning towards resolveDependenciesOfImporters for a similar reason. The catalog: protocol only works on importers, so I thought made sense made sense there too.

Here's the call stack for reference. I generated this a few weeks ago when trying to think through where it made sense.

resolveDependencies (src/index.ts)
  resolveDependencyTree
    resolveRootDependencies
      resolveDependenciesOfImporters
        resolveDependenciesOfDependency
          resolveDependency
            resolveAndFetch - ctx.storeController.requestPackage
              createResolver() - default-resolver
      resolveDependencies (src/resolveDependencies.ts)

One other reason that I settled on resolveDependency is because this made it easy to populate the catalogMetadata field on the response of a specific dependency which is used to generate the catalogs section of the lock file.

I can try that on resolveDependenciesOfImporters instead, but a bit worried it might be come out messier than in resolveDependency.

I do think we should throw a clear error when the catalog: protocol is seen in an external dependency. Was going to work on that in a different PR.

@gluxon
Copy link
Member Author

gluxon commented Jun 17, 2024

I'm recalling a big reason I put the replacement in resolveDependency is because that's where the existing wantedDependency.pref replacement was happening, so it made sense in my mind to keep those together.

if (catalogLookup != null) {
wantedDependency.pref = catalogLookup.specifier
}
if (!options.update && currentPkg.version && currentPkg.pkgId?.endsWith(`@${currentPkg.version}`)) {
wantedDependency.pref = replaceVersionInPref(wantedDependency.pref, currentPkg.version)
}

@zkochan
Copy link
Member

zkochan commented Jun 17, 2024

I'm recalling a big reason I put the replacement in resolveDependency is because that's where the existing wantedDependency.pref replacement was happening, so it made sense in my mind to keep those together.

That's a different thing though. That one is applied on all levels, while catalogs are only applied on importers.

I was also leaning towards resolveDependenciesOfImporters for a similar reason. The catalog: protocol only works on importers, so I thought made sense made sense there too.

That function sounds good as well.

@gluxon
Copy link
Member Author

gluxon commented Jun 17, 2024

Thanks @zkochan. I have to head out for a few hours, but will update this PR with that improvement when I get back tonight. 👍

@gluxon gluxon force-pushed the gluxon/catalog-protocol-install branch from 4bf157f to 8db6388 Compare June 20, 2024 08:16
@zkochan
Copy link
Member

zkochan commented Jun 20, 2024

Looks like compilation fails.

@gluxon
Copy link
Member Author

gluxon commented Jun 20, 2024

Sorry about that — fixing now.

@gluxon gluxon force-pushed the gluxon/catalog-protocol-install branch 3 times, most recently from 32c090d to 80a9036 Compare June 20, 2024 16:12
})

if (catalogLookup != null) {
extendedWantedDep.wantedDependency.pref = catalogLookup.specifier
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't love the mutation happening here, but following a similar pattern in resolveDependencies, which we were discussing a few days ago.

wantedDependency.pref = replaceVersionInPref(wantedDependency.pref, currentPkg.version)

Let me know if we should recreate the extendedWantedDep object instead.

// If the catalog protocol was used, store metadata about the catalog
// lookup to use in the lockfile.
if (result.resolveDependencyResult != null && catalogLookup != null) {
result.resolveDependencyResult.catalogLookup = catalogLookup
Copy link
Member Author

Choose a reason for hiding this comment

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

I similarly don't love the mutation here. Open to ideas on alternatives.

@gluxon gluxon force-pushed the gluxon/catalog-protocol-install branch 2 times, most recently from 93143aa to 80a9036 Compare June 21, 2024 04:02
@gluxon gluxon force-pushed the gluxon/catalog-protocol-install branch from 80a9036 to e6a09b8 Compare June 23, 2024 17:12
@gluxon
Copy link
Member Author

gluxon commented Jun 23, 2024

Latest push removes the redundant newline and nested function.

Let me get back to you about simplifying getCatalogSnapshotsForResolvedDeps. Have to head out for a few hours but will be back to look at that soon today.

@gluxon gluxon force-pushed the gluxon/catalog-protocol-install branch from e6a09b8 to d348749 Compare June 23, 2024 17:17
@yyx990803 yyx990803 mentioned this pull request Jun 24, 2024
18 tasks
@gluxon gluxon force-pushed the gluxon/catalog-protocol-install branch from 3141668 to 05993a8 Compare June 25, 2024 07:48
@gluxon gluxon force-pushed the gluxon/catalog-protocol-install branch from 05993a8 to 0a65b30 Compare June 25, 2024 20:52
@gluxon
Copy link
Member Author

gluxon commented Jun 25, 2024

Latest push should address all feedback so far except:

@zkochan
Copy link
Member

zkochan commented Jun 25, 2024

ok, I'll probably check tomorrow.

Comment on lines +32 to +34
const { allProjects } = await filterPackagesFromDir(
this.workspaceDir,
opts?.filter?.map(parentDir => ({ parentDir })) ?? [])
Copy link
Member

Choose a reason for hiding this comment

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

why do you need the filterPackagesFromDir function? All the data is already available. The tests in core are low level. You already passed all the required objects to preparePackages.

You can see in multipleImporters.ts tests how it is done. I think duplicating code in tests is fine. It is easier to understand. While helpers force to additionally learn how the helpers work.

Copy link
Member

Choose a reason for hiding this comment

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

merged accidentally. Still, filterPackagesFromDir is not needed as a new dev dep in core

Copy link
Member Author

Choose a reason for hiding this comment

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

Got it. I'll remove this from core in a PR now.

why do you need the filterPackagesFromDir function? All the data is already available. The tests in core are low level. You already passed all the required objects to preparePackages.

I wanted something that re-read the manifests off of disk after the calling the updateProjectManifest method. I also wanted to avoid other people having to edit this test if something about the Project interface changes.

You can see in multipleImporters.ts tests how it is done. I think duplicating code in tests is fine. It is easier to understand. While helpers force to additionally learn how the helpers work.

I agree with this principle. Sometimes helpers do make code harder to understand.

Copy link
Member Author

@gluxon gluxon Jun 27, 2024

Choose a reason for hiding this comment

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

@zkochan zkochan merged commit 413721e into catalogs Jun 26, 2024
@zkochan zkochan deleted the gluxon/catalog-protocol-install branch June 26, 2024 13:50
@gluxon gluxon mentioned this pull request Jun 26, 2024
gluxon added a commit that referenced this pull request Jun 27, 2024
* feat: add catalogs to @pnpm/config

* refactor: factor out resolveDependenciesOfImporterDependency function

* feat: implement catalog resolver and replace prefs

* revert: work around CI setup not compiling orphan packages before testing

* feat: record catalog lookup snapshots through propagated metadata

* feat: update projects when catalogs config changes

* test: add catalog protocol install tests
zkochan added a commit that referenced this pull request Jun 27, 2024
* feat: create new @pnpm/catalogs.types package (#8026)

* feat: read catalog configs from workspace manifest (#8123)

* refactor: move InvalidWorkspaceManifestError to its own file

* feat: read catalogs config from workspace manifest

* chore: add changeset for new catalog config parsing

* feat: create new `@pnpm/catalogs.protocol-parser` package (#8124)

This works around a problem with pnpm's CI setup not compiling
packages that are not dependencies of the main pnpm package before
running these tests.

#8027 (comment)

* refactor: factor out isWantedDepPrefSame to extend in a future commit (#8125)

* feat: create new `@pnpm/catalogs.config` package (#8220)

* refactor: remove single default catalog check

This check will happen in  `@pnpm/catalogs.config` instead.

* feat: create new @pnpm/catalogs.config package

* fix: work around CI setup not compiling orphan packages before testing

This works around a problem with pnpm's CI setup not compiling
packages that are not dependencies of the main pnpm package before
running these tests.

#8027 (comment)

* feat: create new `@pnpm/catalogs.resolver` package (#8219)

* feat: create new @pnpm/catalogs.resolver package

* fix: work around CI setup not compiling orphan packages before testing

This works around a problem with pnpm's CI setup not compiling
packages that are not dependencies of the main pnpm package before
running these tests.

#8027 (comment)

* feat: implement catalog protocol for publish (#8225)

* feat: implement catalog protocol for install (#8221)

* feat: add catalogs to @pnpm/config

* refactor: factor out resolveDependenciesOfImporterDependency function

* feat: implement catalog resolver and replace prefs

* revert: work around CI setup not compiling orphan packages before testing

* feat: record catalog lookup snapshots through propagated metadata

* feat: update projects when catalogs config changes

* test: add catalog protocol install tests

* refactor: remove filter-packages-from-dir dependency from core tests (#8244)

* refactor: remove filter-packages-from-dir dependency from core tests

* test: refactor

* test: refactor

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
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.

2 participants