Skip to content

RFC: Packages extensibility #849

@JakeGinnivan

Description

@JakeGinnivan

Problem

The tagling for changesets is A way to manage your versioning and changelogs with a focus on monorepos, currently it is A way to manage your versioning and changelogs for NPM packages with a focus on monorepos

The goal of this RFC is to suggest how we could realise the dream while not only supporting NPM.

There are also a bunch of issues talking about changing behavior around NPM publish.

Related discussions / PRs

#171
#218
#399
#425
#580
#654
#778
#800
#801

Proposed solution

Packages have a few basic components:

  1. A name
  2. A version
  3. Dependencies to other packages in the repo
  4. Publish status with version
  5. Ability to be published

If changesets allows the concept of 'packages' to be extensible then a repository could have multiple types of packages registered. For example in an NX repo you could expose NX projects via the plugin.
It would also allow non-node projects to be tracked by creating a plugin to support Ruby Gems, or .net NuGet packages etc.

Package interface

type PublishedState = "never" | "published" | "only-pre";

interface ChangesetPackage {
  name: string
  version: string
  kind: string
  directory: string
  dependencies: string[]

  getPublishInfo(): Promise<{
   publishedState: PublishedState
   publishedVersions: string[]
  }>
  /** assumes updateVersion has run */
  publish(): Promise<{}>
  updateVersion(version: string): Promise<{}>
}

Config

  "packages": [
    "@changesets/packages-nx",
    [
      "@changesets/packages-npm",
      { "repository": "npm", ignore: ['some-package'] }
    ],
    [
      "@changesets/packages-nuget",
      { }
    ]
  ]

Core workflows

Some of these diagrams could be improved, but is a first pass at visualising the workflows. Also have left out things like pre-release to keep things simple.

Get Packages

sequenceDiagram
    cli ->>+ getPackages: 
    getPackages ->> config: getPackagesPlugins
    config ->> getPackages: PackagePlugin[]
    loop map each plugin
        getPackages ->> plugin: getPackages
        plugin ->> getPackages: ChangesetPackage[]
    end

    note over getPackages: Input ChangesetPackage[][]
    loop flatMap packages
        alt !exists in map
            getPackages ->> getPackages: add to map
        end
    end

    getPackages ->>- cli: ChangesetPackage[]

Loading

Version

sequenceDiagram
    note over getPackages: *Changed
    version ->> getPackages: 
    getPackages ->> version: ChangesetPackage[]
    version ->> readChangeset: 
    readChangeset ->> version: NewChangeset[]

    version ->> assembleReleasePlan: changesets, packages
    note over assembleReleasePlan: Filters changesets from ignored packages
    note over assembleReleasePlan: Flatten changesets into package releases
    note over assembleReleasePlan: Add changes due to dependencies into releases
    assembleReleasePlan ->> version: { changesets: NewChangeset[], releases: ComprehensiveRelease[] }

    version ->> applyReleasePlan: 
    
    loop for each release
        note over package: *Changed from directly updating package.json
        applyReleasePlan ->> package: updateVersion()
        applyReleasePlan ->> config: Get changelog plugin
        note over applyReleasePlan: Gets commit for each change
        note over applyReleasePlan: Uses plugin to format lines

        applyReleasePlan ->> changeset file: Updates CHANGELOG.md for project
    end
Loading

Publish

This function gets rejigged a fair bit. Packages marked as private never call the publish() function on the package, they are just tagged (assuming the config allows that).

sequenceDiagram
    publish ->> getPackages: 
    getPackages ->> version: ChangesetPackage[]
    
    loop parallel map for each package
        alt is private package
            publish ->> git: get tag for current version
            alt tag exists
                publish -x publish: do nothing
                note over publish: return { published: false }
            else
                publish ->> git: tag release
                note over publish: return { published: true }
            end
        else
            publish ->> package: publish()
            alt success
                publish ->> git: tag release
                note over publish: return { published: true }
            end
        end
    end
Loading

Challenges

Source of truth?

While building #662 one of the challenges of tracking private packages is that Changeset uses the published NPM info as the source of truth but this isn't possible for the private packages. Those instead use the git tags in the repo.

I think we should use tags in the git repo as our state, then we can put checks in place to 'refresh' if things get out of sync, ie publish succeeds but then tagging fails. We can detect and fix this state.

This approach will not work if you can publish without tagging though, or not tag per repo. Either that or only private packages rely on git tags being the source of truth?

Duplicate discovery

Package discovery will be done by multiple plugins, if multiple plugins discover a project I think we simply take the one from the plugin registered first (so ordering of the plugins matters).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions