-
Notifications
You must be signed in to change notification settings - Fork 109
docs: add contributor docs for multi version #2443
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
Merged
+307
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,307 @@ | ||
| --- | ||
| title: Multi-Version System | ||
| category: Contributor Guides | ||
| order: 8 | ||
| --- | ||
|
|
||
| # Multi-Version System | ||
|
|
||
| ## Overview | ||
|
|
||
| When we need to introduce breaking changes to a component, we don't modify the existing version in place. Instead, we create a new version of the component in a separate folder, preserving the old version as-is. This ensures that consumers on older library versions can upgrade to new versions of InstUI without breaking changes. | ||
|
|
||
| Each component version lives in a numbered directory (`v1`, `v2`, `v3`, ...) inside the component's own folder under `src/` (e.g., `packages/ui-alerts/src/Alert/v1/`, `packages/ui-alerts/src/Alert/v2/`). A system of lettered export files and `package.json` exports ties these versions to specific library minor versions. | ||
|
|
||
| ## Key Concepts | ||
|
|
||
| - **Component version** (`v1`, `v2`, `v3`): A directory containing a complete, self-contained implementation of a component. | ||
| - **Lettered export file** (`a.ts`, `b.ts`, `c.ts`): A file under `src/exports/` that re-exports all components in a package at specific versions. Each letter represents a snapshot of the package's public API. | ||
| - **Library version** (`v11_6`, `v11_7`): A minor version of the InstUI library. Each library version maps to a lettered export in every package. | ||
| - **`latest` export path**: Always points to the newest lettered export. Used by the newest component versions to reference other packages at their most current implementation. | ||
| - Multiple library versions can point to the same component version (e.g., both `v11_6` and `v11_7` may export Alert from `v1` if no breaking change was introduced). | ||
| - **Default (`"."`) export**: Always points to the **oldest** lettered export (`a.ts`). This ensures that consumers who upgrade without changing their import paths get the version they were already using, preventing unexpected breakage. | ||
|
|
||
| ## How It All Fits Together | ||
|
|
||
| Take `@instructure/ui-alerts` as an example: | ||
|
|
||
| ```sh | ||
| --- | ||
| type: code | ||
| --- | ||
| packages/ui-alerts/src/ | ||
| ├── Alert/ | ||
| │ ├── v1/ # Original implementation | ||
| │ │ ├── index.tsx | ||
| │ │ ├── props.ts | ||
| │ │ ├── README.md | ||
| │ │ ├── styles.ts | ||
| │ │ └── theme.ts | ||
| │ └── v2/ # Breaking changes introduced here | ||
| │ ├── index.tsx | ||
| │ ├── props.ts | ||
| │ ├── README.md | ||
| │ ├── styles.ts | ||
| │ ├── theme.ts | ||
| │ └── __tests__/ | ||
| │ └── Alert.test.tsx | ||
|
Comment on lines
+46
to
+47
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dont forget to change this if you move the tests |
||
| ├── exports/ | ||
| │ ├── a.ts # Exports Alert from v1 | ||
| │ └── b.ts # Exports Alert from v2 | ||
| ``` | ||
|
|
||
| The package's `package.json` `exports` field maps library versions to lettered exports: | ||
|
|
||
| ```json | ||
| --- | ||
| type: code | ||
| --- | ||
| { | ||
| "exports": { | ||
HerrTopi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "./lib/*": "./lib/*", | ||
| "./es/*": "./es/*", | ||
| "./types/*": "./types/*", | ||
| "./package.json": "./package.json", | ||
| "./src/*": "./src/*", | ||
| ".": { | ||
| "src": "./src/exports/a.ts", | ||
| "types": "./types/exports/a.d.ts", | ||
| "import": "./es/exports/a.js", | ||
| "require": "./lib/exports/a.js", | ||
| "default": "./es/exports/a.js" | ||
| }, | ||
| "./v11_6": { | ||
| "src": "./src/exports/a.ts", | ||
| "types": "./types/exports/a.d.ts", | ||
| "import": "./es/exports/a.js", | ||
| "require": "./lib/exports/a.js", | ||
| "default": "./es/exports/a.js" | ||
| }, | ||
| "./v11_7": { | ||
| "src": "./src/exports/b.ts", | ||
| "types": "./types/exports/b.d.ts", | ||
| "import": "./es/exports/b.js", | ||
| "require": "./lib/exports/b.js", | ||
| "default": "./es/exports/b.js" | ||
| }, | ||
| "./latest": { | ||
| "src": "./src/exports/b.ts", | ||
| "types": "./types/exports/b.d.ts", | ||
| "import": "./es/exports/b.js", | ||
| "require": "./lib/exports/b.js", | ||
| "default": "./es/exports/b.js" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| - `"."` → `a.ts` (oldest, for backwards compatibility) | ||
| - `./v11_6` → `a.ts` (Alert v1) | ||
| - `./v11_7` → `b.ts` (Alert v2) | ||
| - `./latest` → `b.ts` (always the newest) | ||
|
|
||
| ## Adding a New Component Version | ||
|
|
||
| ### When to create a new version | ||
|
|
||
| Create a new component version whenever you need to introduce a **breaking change** — removing/renaming props, changing behavior, altering types, removing theme variables, etc. | ||
|
|
||
| ### Step-by-step | ||
|
|
||
| We'll walk through an example: adding a `v3` of `Alert` in `@instructure/ui-alerts`, assuming the current library version is `11.7`. | ||
|
|
||
| #### 1. Copy the current latest version | ||
|
|
||
| Copy the entire contents of the current latest version directory to a new one: | ||
|
|
||
| ```sh | ||
| --- | ||
| type: code | ||
| --- | ||
| packages/ui-alerts/src/Alert/v2/ → packages/ui-alerts/src/Alert/v3/ | ||
| ``` | ||
|
|
||
| The new version (`v3`) should contain everything the previous version had: `index.tsx`, `props.ts`, `README.md`, `styles.ts`, `theme.ts`, `__tests__/`, etc. | ||
|
|
||
| #### 2. Freeze the old version | ||
|
|
||
| The version that was previously the latest (`v2` in this case) must be frozen: | ||
|
|
||
| 1. **Delete its tests.** Only the latest version is tested and supported. Remove the `__tests__/` directory from `v2`. | ||
|
|
||
| 2. **Pin internal cross-package imports.** Any imports from other versioned `@instructure/*` packages that use the `/latest` subpath must be changed to the current released version. For example: | ||
|
|
||
| ```diff | ||
| --- | ||
| type: code | ||
| --- | ||
| - import { View } from '@instructure/ui-view/latest' | ||
| + import { View } from '@instructure/ui-view/v11_7' | ||
| ``` | ||
|
|
||
| This freezes the old version to a known, working state. Only packages that have versioning (i.e., those with `./vX_Y` entries in their `exports` field) need this treatment. Non-versioned packages like `@instructure/emotion` are left as-is. | ||
|
|
||
| #### 3. Make breaking changes in the new version | ||
|
|
||
| In `v3`, make whatever breaking changes are needed. The new version's internal imports should use `/latest` subpaths to always reference the newest implementations of other packages: | ||
|
|
||
| ```ts | ||
| --- | ||
| type: code | ||
| --- | ||
| import { View } from '@instructure/ui-view/latest' | ||
| ``` | ||
|
|
||
| #### 4. Create a new lettered export file | ||
|
|
||
| Add a new export file under `src/exports/`. The letter increments alphabetically (`a.ts` → `b.ts` → `c.ts`). | ||
|
|
||
| This file must export **all** components in the package at their latest versions — not just the one that changed. If `ui-alerts` also exported an `InlineAlert` that didn't get a new version, the new export file still includes it: | ||
|
|
||
| ```ts | ||
| --- | ||
| type: code | ||
| --- | ||
| // src/exports/c.ts | ||
| export { Alert } from '../Alert/v3' | ||
| export type { AlertProps } from '../Alert/v3/props' | ||
| export { InlineAlert } from '../InlineAlert/v2' // unchanged, still at its latest | ||
| export type { InlineAlertProps } from '../InlineAlert/v2/props' | ||
| ``` | ||
|
|
||
| #### 5. Update `package.json` exports | ||
|
|
||
| Check the monorepo-wide version (every `package.json` shares the same `version` field). If the current version is `11.7`, the new library version is `11.8`. | ||
|
|
||
| Add the new version entry and update `./latest`: | ||
|
|
||
| ```json | ||
| --- | ||
| type: code | ||
| --- | ||
| { | ||
| "exports": { | ||
| "./lib/*": "./lib/*", | ||
| "./es/*": "./es/*", | ||
| "./types/*": "./types/*", | ||
| "./package.json": "./package.json", | ||
| "./src/*": "./src/*", | ||
| ".": { | ||
| "src": "./src/exports/a.ts", | ||
| "types": "./types/exports/a.d.ts", | ||
| "import": "./es/exports/a.js", | ||
| "require": "./lib/exports/a.js", | ||
| "default": "./es/exports/a.js" | ||
| }, | ||
| "./v11_6": { | ||
| "src": "./src/exports/a.ts", | ||
| "types": "./types/exports/a.d.ts", | ||
| "import": "./es/exports/a.js", | ||
| "require": "./lib/exports/a.js", | ||
| "default": "./es/exports/a.js" | ||
| }, | ||
| "./v11_7": { | ||
| "src": "./src/exports/b.ts", | ||
| "types": "./types/exports/b.d.ts", | ||
| "import": "./es/exports/b.js", | ||
| "require": "./lib/exports/b.js", | ||
| "default": "./es/exports/b.js" | ||
| }, | ||
| "./v11_8": { | ||
| "src": "./src/exports/c.ts", | ||
| "types": "./types/exports/c.d.ts", | ||
| "import": "./es/exports/c.js", | ||
| "require": "./lib/exports/c.js", | ||
| "default": "./es/exports/c.js" | ||
| }, | ||
| "./latest": { | ||
| "src": "./src/exports/c.ts", | ||
| "types": "./types/exports/c.d.ts", | ||
| "import": "./es/exports/c.js", | ||
| "require": "./lib/exports/c.js", | ||
| "default": "./es/exports/c.js" | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Multi-component packages | ||
|
|
||
| A package may contain multiple components (e.g., a hypothetical `ui-alerts` with both `Alert` and `InlineAlert`). Breaking changes can happen to each component independently. | ||
|
|
||
| ### Cascading breakage | ||
HerrTopi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Because every latest component version imports other packages via the `/latest` subpath, introducing a breaking change to one component can break other components that depend on it. This is normal and expected. | ||
|
|
||
| However, a broken build alone is **not** sufficient reason to create a new version of the affected components. A new version is only warranted if the fix required to adapt to the upstream change would itself be a breaking change. If the fix is purely internal and doesn't affect the component's API or visual output, simply update the latest version in place. | ||
|
|
||
| For example, suppose `View` gets a `v2` that removes a prop. `Alert` imports `View` via `/latest`, so after the change it receives the new `View` and it breaks in some way. If fixing `Alert` only requires internal adjustments (e.g., stopping use of the removed prop) without changing Alert's own API or visual output, just update Alert's latest version in place. But if the fix would alter Alert's props or visual appearance in a way that could affect consumers, then a new Alert version (`v3`) is needed. | ||
|
|
||
| ## Testing | ||
HerrTopi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Only the latest version of each component is tested and supported. Older versions are frozen and considered stable. When you create a new version, ensure the `__tests__/` directory lives in the new version's folder and that all tests pass: | ||
|
|
||
| ```bash | ||
| --- | ||
| type: code | ||
| --- | ||
| pnpm run test:vitest ui-alerts | ||
| ``` | ||
|
|
||
| Both Cypress component tests and visual regression tests use the `/latest` versions, so they always test the newest implementation. If they break after a change, they need to be fixed as part of that change. | ||
|
|
||
| ## Versioning and Releases | ||
|
|
||
| Traditional semantic versioning does not apply to the **InstUI library**. Instead: | ||
|
|
||
| - **Breaking changes** are released as **minor** version bumps (e.g., `11.7` → `11.8`). | ||
| - **Everything else** (bug fixes, new features, non-breaking changes) is released as a **patch** bump (e.g., `11.7.0` → `11.7.1`). | ||
| - **Major version bumps** are never triggered automatically from commit messages. They must be initiated manually (`pnpm run bump --releaseType major`) and will be extremely rare. A major bump would be used for fundamental changes that cannot be handled by the multi-version system — such as dropping support for a React version, overhauling the theming architecture, or housekeeping when accumulated component versions grow too numerous and need to be cleaned up. | ||
|
|
||
| ### The bump script | ||
|
|
||
| Releases are prepared by running: | ||
|
|
||
| ```bash | ||
| --- | ||
| type: code | ||
| --- | ||
| pnpm run bump | ||
| ``` | ||
|
|
||
| This script calculates the next version based on the commits since the last release. If the next version is a **patch**, it simply increments the patch number across all packages — no export changes are needed. | ||
|
|
||
| If the next version is a **minor** bump (e.g., `11.7` → `11.8`), the script does additional work: | ||
|
|
||
| 1. **Updates every `ui-*` package's `package.json`:** For each package that does not already have a `./v11_8` entry in its `exports` field, the script adds one. For packages where nothing changed, `./v11_8` points to the same lettered export as `./v11_7` — it's effectively a copy. | ||
|
|
||
| 2. **Updates the `@instructure/ui` meta package:** If there is no `src/v11_8.ts` file yet, the script creates one by copying `src/v11_7.ts` and replacing all `/v11_7` import subpaths with `/v11_8`. For example: | ||
|
|
||
| ```diff | ||
| --- | ||
| type: code | ||
| --- | ||
| - export { Alert } from '@instructure/ui-alerts/v11_7' | ||
| + export { Alert } from '@instructure/ui-alerts/v11_8' | ||
| ``` | ||
|
|
||
| If a `v11_8` entry or file already exists (because a developer manually added it when introducing a breaking change or a new component), the script leaves it as-is. | ||
|
|
||
| ### Why entries may already exist | ||
|
|
||
| When you add a breaking change to a component, you manually add the new `./v11_8` entry to that package's exports (as described in the step-by-step above). The bump script then fills in the same entry for all the other packages that don't have it yet. Similarly, if a new component is added to the umbrella package, you may have already created the `v11_8.ts` file in `@instructure/ui` — the bump script won't overwrite it. | ||
|
|
||
| ## Codemods | ||
|
|
||
HerrTopi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - TODO: Document codemods for migrating consumers between component versions. | ||
| - TODO: Add a codemod that validates all internal cross-package imports use proper versioned or `/latest` subpaths. | ||
|
|
||
| ## Summary of steps needed to add a new version of a component | ||
|
|
||
| 1. **Copy** the current latest version to a new `vX` directory | ||
| 2. **Freeze** the old version: delete tests, pin `/latest` imports to the current released version | ||
| 3. **Implement** breaking changes in the new version (keep `/latest` imports) | ||
| 4. **Create** a new lettered export file that exports all components at their latest versions | ||
| 5. **Update** `package.json` exports: add the new `./vX_Y` entry and point `./latest` to the new letter | ||
HerrTopi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| For information on how documentation is versioned alongside components, see the [Docs Versioning](/#contributor-guides/#docs-versioning) guide. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.