RFC: Packages and versioning
See original GitHub issuePreface
Our current packaging and versioning strategy hinders both consumers and development. To improve the modularity and composability of our components, we should publish packages that are far more granular.
Scope
This RFC focuses specifically on improving how HIG distributes components. Subjects such as project structure, design tokens, and CSS versioning should be discussed in other RFCs.
Issue
HIG’s current packages are based on the different interfaces to components, instead of the components themselves. As a result, components are only available in bundles under a single version, and modularity and composability are limited.
Import example of existing packages:
import { Button as VanillaButton } from 'hig-vanilla';
import { Button } from 'hig-react';
import * as Hig from 'hig-react';
Case
Lets use bundle size optimization as an example. Consumers should be able to only import what’s necessary to maintain small bundle sizes.
When tree-shaking is supported (WIP), this issue will be resolved for some consumers. However, tree-shaking also needs to be supported by the consumer’s build system to be utilized. Specifically, consumers using CommonJS modules will see no benefit from tree-shaking support.
This RFC’s proposal aims to allow components to be consumed individually, regardless of the consumer’s build system.
Proposal
To improve the modularity and composability of our components, we should migrate our packaging strategy to use:
Scoped component packages, e.g. @hig/button
import Button from '@hig/button';
import VanillaButton from '@hig/button/vanilla';
Scoped aggregate package, i.e. @hig/components
import { Button } from '@hig/components';
import Button from '@hig/components/button';
import * as Hig from '@hig/components';
Utilities package, e.g. @hig/utilities
(the name doesn’t really matter)
This package should contain anything that’s shared by multiple components. This package would exist so that each component doesn’t bundle the same utilities over and over. Facebook has a similar package with a clear, detailed explanation for the reasoning behind it.
While being a public package, the API should be considered unstable, and always published with an alpha version.
Private packages, e.g. @hig/playground
All local development tooling and integration tests can be moved into unpublished, private packages.
Migration
We can gradually migrate existing components into separate packages while maintaining support of the existing non-scoped packages (i.e. hig-react
and hig-vanilla
). Once all of the new packages are published, the non-scoped packages can be deprecated.
To clarify, deprecation does not imply breaking changes or abandonment. We should provide support for these packages as long as there’s substantial usage.
Outline:
- Move existing components into new scoped packages
- Consolidate all modules related to a particular component into one package
- e.g. Vanilla component, React adapter, React facade component, etc.
- Consolidate all modules related to a particular component into one package
- Recompose non-scoped packages from the new scoped packages
- No breaking changes
- Deprecate but maintain support
- Compose scoped aggregate package containing all components
- Should only contain components.
- No colors, typography etc. These should be design tokens, but that’s for another RFC. For now, we can put these in
@hig/utilities
.
Versioning
Generally speaking, JavaScript versioning is a solved problem.
Modularity and composability are staples of the community now, and SemVer has been widely adopted to facilitate the use of highly composable modules. HIG’s codebase is a monorepo using an independent versioning scheme. However, to properly leverage semver, our packages should be much more granular.
Using component packages with the aggregate package
Each component should have it’s own version. A change in a particular component shouldn’t result in a version change for a completely unrelated component.
This also provides an upgrade path for consumers. Imagine a scenario where a there’s a breaking change for the Button
component (relax, this is hypothetical). Consumers will be able to easily use a previous version of the Button
component while keeping the rest of the components up-to-date.
Example of both types of packages together:
{
"dependencies": {
"@hig/components": "^2.0.0",
"@hig/button": "1.0.0"
}
}
Yet, this reveals another issue of CSS versioning. BEM, though useful, isn’t well suited to handle multiple versions of the same component rendered together. I’ll be making a separate RFC discussing this issue.
Scoped packages
Using scoped packages is an obvious way to group all of our packages together.
Additionally, it provides confidence to consumers that they’re using the correct package. This is especially important as we add more packages in the future. In short, we can help avoid issues like this and this.
Version numbers
We should closely follow SemVer 2.0.0. However, our versions currently differ slightly.
SemVer 2.0.0 defines major version zero as:
Major version zero (0.y.z) is for initial development. Anything may change at any time. The public API should not be considered stable.
It seems we’re treating major version zero as v1.0.0-beta
. Additionally, in practice many consumers expect 0.x.x
versions to be stable APIs without breaking changes in minor versions. I propose that we maintain our existing approach of treating major version zero as v1.0.0-beta
.
Prior art
The strategy proposed in this RFC isn’t new. Notable libraries have been published in this manner for quite a while now.
Issue Analytics
- State:
- Created 6 years ago
- Comments:7 (6 by maintainers)
Top GitHub Comments
I love this Morris. Smart and inspirational. I want propose taking versioning even farther.
tldr; How about this:
Suppose I have a large app that makes extensive use of a fairly complex component such as a multi-select dropdown. Say HIG makes a breaking API change. If I have to go back and touch every instance of the component in my huge app, I may never bother to update to a newer version.
How can I use the new component without having to go back and change all existing instances?
I’ve heard of one approach that allows this called Semantic Import Versioning. This article comes up in a quick google.
@eskfung
I was purposefully vague with “substantial usage”, so we can better define what that means for us. I don’t foresee much difficulty in maintaining
hig-react
as it is, buthig-vanilla
is another story.