feat: add package-first release flow
This commit is contained in:
+22
-16
@@ -2,21 +2,28 @@
|
|||||||
|
|
||||||
This directory is the release-intent ledger for Cadence UI.
|
This directory is the release-intent ledger for Cadence UI.
|
||||||
|
|
||||||
The repo is still an internal/private monorepo, so Changesets is used conservatively:
|
Changesets now drives the package-first release flow for:
|
||||||
|
|
||||||
- track version intent for releasable workspace packages
|
- `@ai-ui/ui`
|
||||||
- keep release notes attached to code changes
|
- `@ai-ui/tokens`
|
||||||
- avoid tagging private packages during early internal iteration
|
|
||||||
- let CI verify release intent before package work merges
|
The two packages move as a fixed version pair. Changesets remains the source of truth for:
|
||||||
|
|
||||||
|
- version intent
|
||||||
|
- consumer-facing release notes
|
||||||
|
- version PR generation
|
||||||
|
- publish coordination for the package pair
|
||||||
|
|
||||||
## CI behavior
|
## CI behavior
|
||||||
|
|
||||||
This repo now has two release-adjacent workflows:
|
This repo now has four release-adjacent workflows:
|
||||||
|
|
||||||
- `Changeset Status` runs on pull requests and checks whether changes to releasable packages
|
- `Changeset Status` runs on pull requests and checks whether changes to releasable packages
|
||||||
include a `.changeset/*.md` entry, while also verifying that `registry/index.json` is current.
|
include a `.changeset/*.md` entry, while also verifying both package and registry consumer flows.
|
||||||
- `Release Version PR` runs on `main` and opens or updates a version PR by running
|
- `Release Version PR` runs on `main` and opens or updates a version PR by running
|
||||||
`pnpm release:version`.
|
`pnpm release:version`.
|
||||||
|
- `Create Release Tag` runs on `main` after the version pair changes and creates `cadence-ui-vX.Y.Z`.
|
||||||
|
- `Publish Packages` runs on `cadence-ui-v*` tags and publishes the package pair.
|
||||||
|
|
||||||
The PR check intentionally focuses on the releasable packages in scope today:
|
The PR check intentionally focuses on the releasable packages in scope today:
|
||||||
|
|
||||||
@@ -62,14 +69,13 @@ Refactor dialog wrapper internals and update story coverage.
|
|||||||
|
|
||||||
## Current release posture
|
## Current release posture
|
||||||
|
|
||||||
The root repo is private and package publishing is not fully wired yet. Until the main release
|
The root repo remains private, but `@ai-ui/ui` and `@ai-ui/tokens` are now treated as published
|
||||||
flow is enabled, treat Changesets as the source of truth for:
|
packages. The automated flow is:
|
||||||
|
|
||||||
- which package versions should move
|
1. merge a changeset-backed package change
|
||||||
- which changes deserve release notes
|
2. merge the version PR generated on `main`
|
||||||
- which internal dependency bumps should be coordinated
|
3. let the tag and publish workflows ship the fixed package pair
|
||||||
|
|
||||||
The current automation stops at version intent, registry metadata refresh, and version PR creation.
|
See [docs/releasing.md](/Users/xd/project/cadence-ui/docs/releasing.md) for the current release
|
||||||
It does not publish to npm, create tags, or assume any package registry credentials exist yet.
|
workflow and [docs/registry.md](/Users/xd/project/cadence-ui/docs/registry.md) for the optional
|
||||||
|
source-copy mode.
|
||||||
See `docs/releasing.md` for the expected workflow around creating and consuming changesets.
|
|
||||||
|
|||||||
@@ -5,7 +5,12 @@
|
|||||||
"access": "restricted",
|
"access": "restricted",
|
||||||
"baseBranch": "main",
|
"baseBranch": "main",
|
||||||
"updateInternalDependencies": "patch",
|
"updateInternalDependencies": "patch",
|
||||||
"fixed": [],
|
"fixed": [
|
||||||
|
[
|
||||||
|
"@ai-ui/tokens",
|
||||||
|
"@ai-ui/ui"
|
||||||
|
]
|
||||||
|
],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"@ai-ui/docs"
|
"@ai-ui/docs"
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@ai-ui/tokens": minor
|
||||||
|
"@ai-ui/ui": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Ship package-first distribution for `@ai-ui/ui` and `@ai-ui/tokens`, including publishable package exports, package consumer validation, and automated tag-to-publish release flow.
|
||||||
@@ -105,3 +105,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Run registry consumer smoke test
|
- name: Run registry consumer smoke test
|
||||||
run: pnpm test:registry:consumer
|
run: pnpm test:registry:consumer
|
||||||
|
|
||||||
|
- name: Run package consumer smoke test
|
||||||
|
run: pnpm test:package:consumer
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
name: Publish Packages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "cadence-ui-v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10.25.0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
registry-url: https://registry.npmjs.org
|
||||||
|
scope: "@ai-ui"
|
||||||
|
|
||||||
|
- name: Require npm token
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ -z "${NPM_TOKEN:-}" ]]; then
|
||||||
|
echo "::error::NPM_TOKEN secret is required to publish @ai-ui packages."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Verify tag matches package version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
expected_tag="$(node ./scripts/release-metadata.mjs --field tag)"
|
||||||
|
|
||||||
|
if [[ "${expected_tag}" != "${GITHUB_REF_NAME}" ]]; then
|
||||||
|
echo "::error::Tag ${GITHUB_REF_NAME} does not match package version tag ${expected_tag}."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run release validation
|
||||||
|
run: pnpm release:validate
|
||||||
|
|
||||||
|
- name: Publish packages
|
||||||
|
run: pnpm release:publish
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
name: Create Release Tag
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-tag:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
|
||||||
|
- name: Detect release version change
|
||||||
|
id: release
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
changed="$(node ./scripts/release-metadata.mjs --before "${{ github.event.before }}" --field changed)"
|
||||||
|
tag="$(node ./scripts/release-metadata.mjs --field tag)"
|
||||||
|
version="$(node ./scripts/release-metadata.mjs --field version)"
|
||||||
|
|
||||||
|
echo "changed=${changed}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Create release tag
|
||||||
|
if: steps.release.outputs.changed == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if git rev-parse --verify "refs/tags/${TAG}" >/dev/null 2>&1; then
|
||||||
|
echo "Tag ${TAG} already exists."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git tag -a "${TAG}" -m "Cadence UI ${VERSION}"
|
||||||
|
git push origin "${TAG}"
|
||||||
|
env:
|
||||||
|
TAG: ${{ steps.release.outputs.tag }}
|
||||||
|
VERSION: ${{ steps.release.outputs.version }}
|
||||||
@@ -37,8 +37,8 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run registry consumer smoke test
|
- name: Run release validation
|
||||||
run: pnpm test:registry:consumer
|
run: pnpm release:validate
|
||||||
|
|
||||||
- name: Open or update version PR
|
- name: Open or update version PR
|
||||||
uses: changesets/action@v1
|
uses: changesets/action@v1
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ default styling with its own tokens, motion recipes, and component contract.
|
|||||||
|
|
||||||
- The foundation, token layer, authoring contract, Storybook docs, and unit coverage are in place.
|
- The foundation, token layer, authoring contract, Storybook docs, and unit coverage are in place.
|
||||||
- The public UI surface now includes the core form and overlay set plus advanced patterns such as `DataTable`, `Command`, `Combobox`, `Sheet`, and `EmptyState`.
|
- The public UI surface now includes the core form and overlay set plus advanced patterns such as `DataTable`, `Command`, `Combobox`, `Sheet`, and `EmptyState`.
|
||||||
- The internal source-copy registry flow is live and validated with `pnpm registry:check` and `pnpm test:registry:consumer`.
|
- The default distribution path is package-first: `@ai-ui/ui` and `@ai-ui/tokens` are versioned and validated for package consumption.
|
||||||
- The main remaining release work is publish policy and tagging automation, not initial component bootstrapping.
|
- The internal source-copy registry flow remains available as an optional mode for teams that want local ownership of copied component source.
|
||||||
|
|
||||||
## System principles
|
## System principles
|
||||||
|
|
||||||
@@ -68,6 +68,7 @@ Run tests:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm test
|
pnpm test
|
||||||
|
pnpm test:package:consumer
|
||||||
pnpm test:registry:consumer
|
pnpm test:registry:consumer
|
||||||
pnpm test:e2e:smoke
|
pnpm test:e2e:smoke
|
||||||
```
|
```
|
||||||
@@ -81,7 +82,13 @@ pnpm typecheck
|
|||||||
|
|
||||||
## Package consumption
|
## Package consumption
|
||||||
|
|
||||||
For package-style consumers, prefer a single CSS entrypoint from `@ai-ui/ui`:
|
Package consumption is the default path for downstream apps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @ai-ui/ui
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefer a single CSS entrypoint from `@ai-ui/ui`:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { Button } from "@ai-ui/ui";
|
import { Button } from "@ai-ui/ui";
|
||||||
@@ -95,14 +102,19 @@ import { Button } from "@ai-ui/ui";
|
|||||||
|
|
||||||
This keeps the app on one UI package import path while still pulling in token and skin
|
This keeps the app on one UI package import path while still pulling in token and skin
|
||||||
styles together. Consumers that want lower-level control can still import
|
styles together. Consumers that want lower-level control can still import
|
||||||
`@ai-ui/tokens/styles.css` and `@ai-ui/ui/skins.css` separately.
|
`@ai-ui/tokens/styles.css` and `@ai-ui/ui/skins.css` separately. If you need token helpers
|
||||||
|
such as `setTheme`, add `@ai-ui/tokens` directly as well.
|
||||||
|
|
||||||
Install source-owned components into another project:
|
If you need source ownership instead of package upgrades, use the optional registry
|
||||||
|
installer to copy component source into another project:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm registry:install --project ../acme-app button dialog
|
pnpm registry:install --project ../acme-app button dialog
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Package release details live in [docs/releasing.md](/Users/xd/project/cadence-ui/docs/releasing.md).
|
||||||
|
Source-copy install and upgrade details live in [docs/registry.md](/Users/xd/project/cadence-ui/docs/registry.md).
|
||||||
|
|
||||||
## Workspace structure
|
## Workspace structure
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
@@ -128,11 +140,11 @@ The system is layered:
|
|||||||
The current public component layer lives in `packages/ui/src/components`, with shared
|
The current public component layer lives in `packages/ui/src/components`, with shared
|
||||||
helpers in `packages/ui/src/lib`.
|
helpers in `packages/ui/src/lib`.
|
||||||
|
|
||||||
## Registry install flow
|
## Optional Source-Copy Flow
|
||||||
|
|
||||||
Cadence UI now ships a minimal internal registry flow for source-owned adoption.
|
Cadence UI still ships a registry installer for teams that want to copy component source
|
||||||
Consumers pin this repo to a reviewed commit or tag, then run the local installer to
|
into their own app and keep editing it there. This is the advanced customization path,
|
||||||
copy selected items into their own codebase.
|
not the default distribution path.
|
||||||
|
|
||||||
- Registry metadata lives in `registry/index.json` and is generated by `pnpm registry:build`.
|
- Registry metadata lives in `registry/index.json` and is generated by `pnpm registry:build`.
|
||||||
- The generated index tracks transitive local helpers in addition to component entrypoints, so helper-import changes need a registry rebuild before merge.
|
- The generated index tracks transitive local helpers in addition to component entrypoints, so helper-import changes need a registry rebuild before merge.
|
||||||
@@ -149,6 +161,7 @@ to document more than the default playground when behavior is non-trivial. The r
|
|||||||
uses:
|
uses:
|
||||||
|
|
||||||
- Vitest + Testing Library for unit and interaction coverage
|
- Vitest + Testing Library for unit and interaction coverage
|
||||||
|
- package consumer smoke coverage for published-package consumption
|
||||||
- Storybook interaction coverage for representative examples
|
- Storybook interaction coverage for representative examples
|
||||||
- Playwright smoke coverage for core Storybook flows
|
- Playwright smoke coverage for core Storybook flows
|
||||||
- Storybook a11y checks as part of the docs review surface
|
- Storybook a11y checks as part of the docs review surface
|
||||||
|
|||||||
+17
-7
@@ -1,8 +1,10 @@
|
|||||||
# Registry Install and Upgrade
|
# Registry Install and Upgrade
|
||||||
|
|
||||||
Cadence UI now supports a minimal internal registry flow for source-owned consumers.
|
Cadence UI supports a source-copy registry flow for teams that want local ownership of
|
||||||
The goal is not package publishing yet. The goal is to make one reviewed repo state
|
component source inside their own application.
|
||||||
directly consumable by another app.
|
|
||||||
|
This is the optional customization path. The default downstream consumption model is now
|
||||||
|
package install via `@ai-ui/ui`, with `@ai-ui/tokens` available for lower-level token imports.
|
||||||
|
|
||||||
## What the registry contains
|
## What the registry contains
|
||||||
|
|
||||||
@@ -52,12 +54,22 @@ required dependencies, and verifies that the copied source both typechecks and b
|
|||||||
|
|
||||||
## Consumer install flow
|
## Consumer install flow
|
||||||
|
|
||||||
|
Choose this flow when:
|
||||||
|
|
||||||
|
- your team wants to edit copied component code locally
|
||||||
|
- you expect app-specific divergence that would be awkward to upstream immediately
|
||||||
|
- you prefer source ownership over centralized package upgrades
|
||||||
|
|
||||||
|
If you just want to use Cadence UI in another app with the lowest maintenance overhead,
|
||||||
|
use the package flow in [docs/releasing.md](/Users/xd/project/cadence-ui/docs/releasing.md)
|
||||||
|
instead.
|
||||||
|
|
||||||
### 1. Pin the Cadence UI source
|
### 1. Pin the Cadence UI source
|
||||||
|
|
||||||
Consumers should install from a reviewed Cadence UI repo state, usually:
|
Consumers should install from a reviewed Cadence UI repo state, usually:
|
||||||
|
|
||||||
- the merge commit of a version PR, or
|
- the `cadence-ui-vX.Y.Z` tag created for the release, or
|
||||||
- a maintainer-created tag that points at that versioned commit
|
- the matching merge commit if the source-copy install must be tested before the tag is created
|
||||||
|
|
||||||
The consumer does not need this repo as a runtime dependency. It only needs access to
|
The consumer does not need this repo as a runtime dependency. It only needs access to
|
||||||
the checked-out source when running the installer.
|
the checked-out source when running the installer.
|
||||||
@@ -130,7 +142,5 @@ This registry flow is intentionally minimal:
|
|||||||
|
|
||||||
It does not yet:
|
It does not yet:
|
||||||
|
|
||||||
- publish to npm
|
|
||||||
- host a remote registry API
|
- host a remote registry API
|
||||||
- auto-tag or auto-publish releases
|
|
||||||
- resolve consumer-specific codemods during upgrade
|
- resolve consumer-specific codemods during upgrade
|
||||||
|
|||||||
+104
-143
@@ -1,47 +1,63 @@
|
|||||||
# Releasing
|
# Releasing
|
||||||
|
|
||||||
This repo is not fully on a public-package release pipeline yet, but it is ready to use
|
Cadence UI now uses a package-first release model.
|
||||||
Changesets as the canonical record of release intent.
|
|
||||||
|
|
||||||
The current goal is modest:
|
Default downstream consumers should install:
|
||||||
|
|
||||||
- version `@ai-ui/ui` and `@ai-ui/tokens` deliberately
|
|
||||||
- keep release notes attached to the changes that caused them
|
|
||||||
- avoid inventing ad hoc version bumps when the component system evolves
|
|
||||||
- make release intent enforceable in CI before package work merges
|
|
||||||
- keep the generated registry metadata aligned with versioned source
|
|
||||||
|
|
||||||
## Current assumptions
|
|
||||||
|
|
||||||
- The repository root is private.
|
|
||||||
- Workspace packages currently use explicit package versions even when they are not yet published.
|
|
||||||
- `@ai-ui/docs` is a consumer app, not a releasable package, so it is ignored by Changesets.
|
|
||||||
- Publishing mechanics and registry credentials are still to be added.
|
|
||||||
|
|
||||||
Because of that, this baseline is intentionally conservative.
|
|
||||||
|
|
||||||
## Packages in scope
|
|
||||||
|
|
||||||
Changesets should currently be used for:
|
|
||||||
|
|
||||||
- `@ai-ui/ui`
|
- `@ai-ui/ui`
|
||||||
- `@ai-ui/tokens`
|
|
||||||
|
|
||||||
Package consumers should now prefer:
|
Add `@ai-ui/tokens` only when the consumer app imports token helpers or lower-level token CSS
|
||||||
|
directly.
|
||||||
|
|
||||||
- `@ai-ui/ui` for component imports
|
The source-copy registry flow still exists, but it is the optional customization path, not the
|
||||||
- `@ai-ui/ui/styles.css` for the combined token + skin stylesheet
|
default release target. See [docs/registry.md](/Users/xd/project/cadence-ui/docs/registry.md)
|
||||||
|
for that mode.
|
||||||
|
|
||||||
The lower-level entries remain available when needed:
|
## Release artifacts
|
||||||
|
|
||||||
- `@ai-ui/tokens/styles.css`
|
Each release now has three artifacts:
|
||||||
- `@ai-ui/ui/skins.css`
|
|
||||||
|
|
||||||
Changes to the docs app alone usually do not need a changeset.
|
- published packages for `@ai-ui/ui` and `@ai-ui/tokens`
|
||||||
|
- a repository tag in the form `cadence-ui-vX.Y.Z`
|
||||||
|
- a committed `registry/index.json` snapshot for source-copy consumers pinned to that tag
|
||||||
|
|
||||||
|
## Package policy
|
||||||
|
|
||||||
|
- `@ai-ui/ui` and `@ai-ui/tokens` are released as a fixed version pair
|
||||||
|
- `@ai-ui/ui` is the primary component import surface
|
||||||
|
- `@ai-ui/ui/styles.css` is the default stylesheet entrypoint for package consumers
|
||||||
|
- `@ai-ui/tokens` remains available for lower-level token helpers and direct CSS imports
|
||||||
|
- `apps/docs` is a consumer app, not a releasable package
|
||||||
|
|
||||||
|
The current workflows assume private scoped packages on the npm registry with `access:
|
||||||
|
restricted`. If this repo moves to another registry later, update the publish workflow and
|
||||||
|
consumer setup accordingly.
|
||||||
|
|
||||||
|
## Consumer install
|
||||||
|
|
||||||
|
Package consumers should install the packages directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @ai-ui/ui
|
||||||
|
```
|
||||||
|
|
||||||
|
Then import from `@ai-ui/ui` and point Tailwind at the packaged source:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Button } from "@ai-ui/ui";
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
@import "tailwindcss";
|
||||||
|
@import "@ai-ui/ui/styles.css";
|
||||||
|
@source "../node_modules/@ai-ui/ui/src";
|
||||||
|
```
|
||||||
|
|
||||||
|
This flow is validated by `pnpm test:package:consumer`.
|
||||||
|
|
||||||
## When to create a changeset
|
## When to create a changeset
|
||||||
|
|
||||||
Create a changeset when a merged change affects any consumer-facing surface of a releasable package:
|
Create a changeset when a merged change affects any consumer-facing package surface:
|
||||||
|
|
||||||
- new components or slots
|
- new components or slots
|
||||||
- changed props or variants
|
- changed props or variants
|
||||||
@@ -63,159 +79,104 @@ If CI would otherwise require a changeset for one of those exceptions, apply the
|
|||||||
|
|
||||||
Use semver pragmatically:
|
Use semver pragmatically:
|
||||||
|
|
||||||
- `patch`: bug fixes, QA-only behavior fixes, docs fixes bundled with a small behavior correction
|
- `patch`: bug fixes, QA-only behavior fixes, or small consumer-visible corrections
|
||||||
- `minor`: new components, new props, new variants, new tokens, additive API work
|
- `minor`: new components, new props, new variants, new tokens, additive API work
|
||||||
- `major`: breaking prop changes, renamed slots or states, removed variants, contract changes that require consumer updates
|
- `major`: breaking contract changes that require consumer updates
|
||||||
|
|
||||||
When in doubt, bias toward `minor` over underselling a visible new surface.
|
Because `@ai-ui/ui` and `@ai-ui/tokens` are fixed together, version bumps move as a pair.
|
||||||
|
|
||||||
## Recommended workflow
|
## Local maintainer commands
|
||||||
|
|
||||||
### 1. Make the code change
|
Run the release validation suite locally with:
|
||||||
|
|
||||||
Complete the implementation, docs, and tests first.
|
|
||||||
|
|
||||||
At minimum, run:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm lint
|
pnpm release:validate
|
||||||
pnpm registry:check
|
|
||||||
pnpm typecheck
|
|
||||||
pnpm test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the docs and smoke checks when the change touches behavior-heavy UI:
|
That command currently runs:
|
||||||
|
|
||||||
```bash
|
- lint
|
||||||
pnpm build:docs
|
- typecheck
|
||||||
pnpm test:registry:consumer
|
- package unit tests
|
||||||
pnpm test:e2e:smoke
|
- package builds
|
||||||
```
|
- registry metadata validation
|
||||||
|
- source-copy consumer smoke
|
||||||
|
- package consumer smoke
|
||||||
|
|
||||||
### 2. Create a changeset
|
Create a versioned release change with:
|
||||||
|
|
||||||
After the change is ready, create a changeset entry for the affected package or packages.
|
|
||||||
|
|
||||||
Once `@changesets/cli` is installed in the repo, the intended command is:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm changeset
|
pnpm changeset
|
||||||
```
|
```
|
||||||
|
|
||||||
The generated markdown file should:
|
Apply pending version bumps locally with:
|
||||||
|
|
||||||
- select the impacted package(s)
|
|
||||||
- choose the correct version bump type
|
|
||||||
- include a short consumer-facing summary
|
|
||||||
|
|
||||||
### 3. Review internal dependency impact
|
|
||||||
|
|
||||||
This repo is configured to update internal dependencies with a patch bump.
|
|
||||||
|
|
||||||
That means if `@ai-ui/tokens` changes and `@ai-ui/ui` depends on it, the versioning step should
|
|
||||||
keep the dependency graph coherent without requiring manual package edits.
|
|
||||||
|
|
||||||
### 4. Version the packages
|
|
||||||
|
|
||||||
When it is time to cut a release manually, run the repo version step:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm release:version
|
pnpm release:version
|
||||||
```
|
```
|
||||||
|
|
||||||
That step is expected to:
|
Publish the packages manually, if needed, with:
|
||||||
|
|
||||||
- update package versions
|
```bash
|
||||||
- update internal dependency ranges where needed
|
pnpm release:publish
|
||||||
- consume the pending changeset files
|
```
|
||||||
- rebuild `registry/index.json` so the copy-in install metadata stays version-aligned
|
|
||||||
|
|
||||||
Review the resulting package diffs carefully before merging.
|
## Automated release flow
|
||||||
|
|
||||||
On `main`, the repository also runs a `Release Version PR` workflow that opens or updates a
|
The automated release sequence is now:
|
||||||
version PR with the same command. That workflow is intentionally limited to version-file updates;
|
|
||||||
it does not publish packages.
|
|
||||||
|
|
||||||
### 5. Distribute the versioned source
|
1. A product or package PR merges to `main` with a changeset.
|
||||||
|
2. `Release Version PR` runs on `main`, executes `pnpm release:validate`, and opens or updates a version PR via Changesets.
|
||||||
|
3. A maintainer reviews and merges that version PR.
|
||||||
|
4. `Create Release Tag` detects the package version change on `main` and creates `cadence-ui-vX.Y.Z`.
|
||||||
|
5. `Publish Packages` runs on the tag push, validates the tagged commit again, and publishes `@ai-ui/ui` and `@ai-ui/tokens`.
|
||||||
|
|
||||||
Publishing is not fully wired in this repo yet, so the consumable artifact is the versioned
|
The tag is the stable source reference for source-copy consumers. The packages are the default
|
||||||
repository state itself.
|
artifact for package consumers.
|
||||||
|
|
||||||
After the version PR merges:
|
|
||||||
|
|
||||||
- keep the merge commit as a stable install point, or create a maintainer tag for it
|
|
||||||
- treat the committed `registry/index.json` as the install contract for that repo state
|
|
||||||
- point consumers at that commit or tag when they run `pnpm registry:install`
|
|
||||||
|
|
||||||
See [docs/registry.md](/Users/xd/project/cadence-ui/docs/registry.md) for the consumer
|
|
||||||
install and upgrade flow.
|
|
||||||
|
|
||||||
## CI workflows
|
## CI workflows
|
||||||
|
|
||||||
### Pull request check
|
### Pull request check
|
||||||
|
|
||||||
The `Changeset Status` workflow runs on pull requests and enforces a simple rule:
|
`Changeset Status` runs on pull requests and:
|
||||||
|
|
||||||
- if a PR changes `@ai-ui/ui` or `@ai-ui/tokens`, it should usually include a new
|
- requires a changeset for releasable package changes unless `no-changeset-needed` is applied
|
||||||
`.changeset/*.md` file
|
- validates pending changesets
|
||||||
- if the work is intentionally non-releasable, maintainers can apply the
|
- checks that `registry/index.json` is current
|
||||||
`no-changeset-needed` label
|
- runs the source-copy consumer smoke test
|
||||||
|
- runs the package consumer smoke test
|
||||||
When a changeset is present, the workflow also runs:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm changeset:status --since origin/main
|
|
||||||
```
|
|
||||||
|
|
||||||
This validates that pending changesets resolve cleanly against the base branch.
|
|
||||||
|
|
||||||
The workflow also runs:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm registry:check
|
|
||||||
```
|
|
||||||
|
|
||||||
This prevents registry metadata drift from merging unnoticed.
|
|
||||||
|
|
||||||
The repo also runs:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm test:registry:consumer
|
|
||||||
```
|
|
||||||
|
|
||||||
This verifies that the committed registry snapshot can still install into a fresh
|
|
||||||
consumer app, typecheck, and build.
|
|
||||||
|
|
||||||
### Version PR workflow
|
### Version PR workflow
|
||||||
|
|
||||||
The `Release Version PR` workflow runs on pushes to `main` and on manual dispatch. It:
|
`Release Version PR` runs on pushes to `main` and:
|
||||||
|
|
||||||
- installs dependencies
|
- installs dependencies
|
||||||
- runs `pnpm release:version`
|
- runs `pnpm release:validate`
|
||||||
- opens or updates a version PR using `changesets/action`
|
- opens or updates the version PR with `pnpm release:version`
|
||||||
|
|
||||||
This is still a versioning skeleton, not a publish pipeline, but the version PR now refreshes
|
### Tag workflow
|
||||||
the committed registry metadata that consumers install from.
|
|
||||||
|
|
||||||
## Future publish prerequisites
|
`Create Release Tag` runs on pushes to `main` and:
|
||||||
|
|
||||||
Before turning the version workflow into a publish workflow, the repo still needs:
|
- detects when the fixed package version pair changed
|
||||||
|
- creates `cadence-ui-vX.Y.Z`
|
||||||
|
|
||||||
- a root publish script, for example `pnpm changeset publish` or a guarded wrapper around it
|
### Publish workflow
|
||||||
- registry credentials such as `NPM_TOKEN`
|
|
||||||
- a decision on which workspace packages should actually be published versus versioned internally
|
`Publish Packages` runs on `cadence-ui-v*` tags and:
|
||||||
- any extra validation that must run before publish is allowed
|
|
||||||
|
- verifies the tag matches the package version
|
||||||
|
- runs `pnpm release:validate`
|
||||||
|
- publishes the packages with `NPM_TOKEN`
|
||||||
|
|
||||||
|
## Required secrets
|
||||||
|
|
||||||
|
The publish workflow currently requires:
|
||||||
|
|
||||||
|
- `NPM_TOKEN`: token with publish access to the `@ai-ui` scope on the configured npm registry
|
||||||
|
|
||||||
## Notes for maintainers
|
## Notes for maintainers
|
||||||
|
|
||||||
- Keep `packages/ui/src/index.ts` and package exports aligned with any release-worthy surface.
|
- Keep `packages/ui/src/index.ts` and package exports aligned with the published surface.
|
||||||
|
- Keep `registry/index.json` in sync even though package publish is now the default; source-copy consumers still rely on it.
|
||||||
- If a component lands without docs or tests, it should not move toward release yet.
|
- If a component lands without docs or tests, it should not move toward release yet.
|
||||||
- Prefer one clear changeset per consumer-visible change rather than bundling unrelated work.
|
|
||||||
- If a PR contains both infra and component work, separate the release notes so consumers can
|
|
||||||
understand what actually changed.
|
|
||||||
|
|
||||||
## Main-thread follow-up still needed
|
|
||||||
|
|
||||||
This baseline now covers changeset intent checks in PRs, registry metadata validation, and
|
|
||||||
version PR creation on `main`. What remains is the publish decision, tagging policy, and any
|
|
||||||
registry-specific automation beyond source-copy installs.
|
|
||||||
|
|||||||
+4
-1
@@ -16,10 +16,13 @@
|
|||||||
"registry:build": "node ./scripts/build-registry.mjs",
|
"registry:build": "node ./scripts/build-registry.mjs",
|
||||||
"registry:check": "node ./scripts/build-registry.mjs --check",
|
"registry:check": "node ./scripts/build-registry.mjs --check",
|
||||||
"registry:install": "node ./scripts/registry-install.mjs",
|
"registry:install": "node ./scripts/registry-install.mjs",
|
||||||
"release:version": "pnpm changeset version && pnpm registry:build",
|
"release:publish": "pnpm build && pnpm changeset publish",
|
||||||
|
"release:validate": "pnpm lint && pnpm typecheck && pnpm test && pnpm build && pnpm registry:check && pnpm test:registry:consumer && pnpm test:package:consumer",
|
||||||
|
"release:version": "pnpm changeset version && pnpm install --lockfile-only && pnpm registry:build",
|
||||||
"test": "pnpm --filter @ai-ui/ui test",
|
"test": "pnpm --filter @ai-ui/ui test",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:smoke": "playwright test tests/e2e/storybook-smoke.spec.ts",
|
"test:e2e:smoke": "playwright test tests/e2e/storybook-smoke.spec.ts",
|
||||||
|
"test:package:consumer": "node ./tests/package-consumer/smoke.mjs",
|
||||||
"test:registry:consumer": "node ./tests/registry/consumer-smoke.mjs",
|
"test:registry:consumer": "node ./tests/registry/consumer-smoke.mjs",
|
||||||
"test:watch": "pnpm --filter @ai-ui/ui test:watch",
|
"test:watch": "pnpm --filter @ai-ui/ui test:watch",
|
||||||
"typecheck": "pnpm -r typecheck"
|
"typecheck": "pnpm -r typecheck"
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "@ai-ui/tokens",
|
"name": "@ai-ui/tokens",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "./dist/index.cjs",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
"sideEffects": [
|
"sideEffects": [
|
||||||
"**/*.css"
|
"**/*.css"
|
||||||
],
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
"./base.css": "./src/base.css",
|
"./base.css": "./src/base.css",
|
||||||
"./motion.css": "./src/motion.css",
|
"./motion.css": "./src/motion.css",
|
||||||
"./styles.css": "./src/styles.css",
|
"./styles.css": "./src/styles.css",
|
||||||
@@ -17,8 +24,11 @@
|
|||||||
"dist",
|
"dist",
|
||||||
"src"
|
"src"
|
||||||
],
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "restricted"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup src/index.ts --clean --dts --format esm --out-dir dist",
|
"build": "tsup src/index.ts --clean --dts --format esm,cjs --out-dir dist",
|
||||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "@ai-ui/ui",
|
"name": "@ai-ui/ui",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "./dist/index.cjs",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
"sideEffects": [
|
"sideEffects": [
|
||||||
"**/*.css"
|
"**/*.css"
|
||||||
],
|
],
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
"./styles.css": "./src/styles.css",
|
"./styles.css": "./src/styles.css",
|
||||||
"./skins.css": "./src/skins.css"
|
"./skins.css": "./src/skins.css"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"src"
|
"src",
|
||||||
|
"!src/**/*.test.ts",
|
||||||
|
"!src/**/*.test.tsx",
|
||||||
|
"!src/test"
|
||||||
],
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "restricted"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup src/index.ts --clean --dts --format esm,cjs --out-dir dist",
|
"build": "tsup src/index.ts --clean --dts --format esm,cjs --out-dir dist",
|
||||||
"test": "vitest run --config ../../vitest.config.ts",
|
"test": "vitest run --config ../../vitest.config.ts",
|
||||||
|
|||||||
+9
-6
@@ -16,12 +16,12 @@ completed most of that baseline work:
|
|||||||
|
|
||||||
- phases 0 through 4 are effectively in place: workspace, tokens, authoring contract, core components, Storybook docs, and baseline tests
|
- phases 0 through 4 are effectively in place: workspace, tokens, authoring contract, core components, Storybook docs, and baseline tests
|
||||||
- phase 5 has shipped its first advanced-pattern slice with `DataTable`, alongside `Command`, `Combobox`, `Sheet`, and `EmptyState`
|
- phase 5 has shipped its first advanced-pattern slice with `DataTable`, alongside `Command`, `Combobox`, `Sheet`, and `EmptyState`
|
||||||
- phase 6 has shipped its baseline source-copy registry flow, Changesets gating, and version PR automation
|
- phase 6 now covers package-first release automation, fixed-version package publishing, and the optional source-copy registry flow
|
||||||
|
|
||||||
The next work is mostly hardening and distribution:
|
The next work is mostly hardening and distribution:
|
||||||
|
|
||||||
- keep the registry metadata and consumer smoke flow reliable
|
- keep the registry metadata and consumer smoke flow reliable
|
||||||
- decide publish and tagging policy
|
- keep package publish validation and tag automation reliable
|
||||||
- expand advanced patterns only where real product usage justifies them
|
- expand advanced patterns only where real product usage justifies them
|
||||||
|
|
||||||
## Product Principles
|
## Product Principles
|
||||||
@@ -368,7 +368,7 @@ Exit criteria:
|
|||||||
## Phase 6: Internal Registry and Distribution
|
## Phase 6: Internal Registry and Distribution
|
||||||
|
|
||||||
Status:
|
Status:
|
||||||
Baseline source-copy distribution is shipped. Publish automation is still open.
|
Package-first publishing and optional source-copy distribution are both in place.
|
||||||
|
|
||||||
Objective:
|
Objective:
|
||||||
Create an internal distribution model similar to `shadcn`, but based on our own rules.
|
Create an internal distribution model similar to `shadcn`, but based on our own rules.
|
||||||
@@ -382,6 +382,9 @@ Deliverables:
|
|||||||
|
|
||||||
Delivered so far:
|
Delivered so far:
|
||||||
|
|
||||||
|
- fixed-version package release pair for `@ai-ui/ui` and `@ai-ui/tokens`
|
||||||
|
- package consumer smoke validation
|
||||||
|
- tag-driven publish workflow
|
||||||
- generated `registry/index.json`
|
- generated `registry/index.json`
|
||||||
- local `pnpm registry:install` copy-in workflow
|
- local `pnpm registry:install` copy-in workflow
|
||||||
- `pnpm test:registry:consumer` validation
|
- `pnpm test:registry:consumer` validation
|
||||||
@@ -389,13 +392,13 @@ Delivered so far:
|
|||||||
|
|
||||||
Remaining follow-up:
|
Remaining follow-up:
|
||||||
|
|
||||||
- publish decision and package registry posture
|
- package registry operational hardening
|
||||||
- tagging policy for stable install points
|
|
||||||
- any automation beyond source-copy installs
|
- any automation beyond source-copy installs
|
||||||
|
|
||||||
Exit criteria:
|
Exit criteria:
|
||||||
|
|
||||||
- consumers can pull components from our registry
|
- package consumers can install the published packages
|
||||||
|
- source-copy consumers can pull components from our registry
|
||||||
- upgrades are documented and reproducible
|
- upgrades are documented and reproducible
|
||||||
|
|
||||||
## Testing Strategy
|
## Testing Strategy
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const repoRoot = path.resolve(scriptDir, "..");
|
||||||
|
const uiPackagePath = path.join(repoRoot, "packages", "ui", "package.json");
|
||||||
|
const tokensPackagePath = path.join(repoRoot, "packages", "tokens", "package.json");
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const options = {
|
||||||
|
before: null,
|
||||||
|
field: null
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let index = 0; index < argv.length; index += 1) {
|
||||||
|
const current = argv[index];
|
||||||
|
|
||||||
|
if (current === "--before" || current === "--field") {
|
||||||
|
const next = argv[index + 1];
|
||||||
|
|
||||||
|
if (!next) {
|
||||||
|
throw new Error(`Expected a value after ${current}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current === "--before") {
|
||||||
|
options.before = next;
|
||||||
|
} else {
|
||||||
|
options.field = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown argument: ${current}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readVersion(filePath) {
|
||||||
|
return JSON.parse(fs.readFileSync(filePath, "utf8")).version;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readVersionAtRef(ref, filePath) {
|
||||||
|
if (!ref || /^0{40}$/.test(ref)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = execFileSync("git", ["show", `${ref}:${path.relative(repoRoot, filePath)}`], {
|
||||||
|
cwd: repoRoot,
|
||||||
|
encoding: "utf8"
|
||||||
|
});
|
||||||
|
return JSON.parse(content).version;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReleaseMetadata(beforeRef) {
|
||||||
|
const uiVersion = readVersion(uiPackagePath);
|
||||||
|
const tokensVersion = readVersion(tokensPackagePath);
|
||||||
|
|
||||||
|
if (uiVersion !== tokensVersion) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected @ai-ui/ui and @ai-ui/tokens to share a version. Received ${uiVersion} and ${tokensVersion}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousUiVersion = readVersionAtRef(beforeRef, uiPackagePath);
|
||||||
|
const previousTokensVersion = readVersionAtRef(beforeRef, tokensPackagePath);
|
||||||
|
const changed =
|
||||||
|
previousUiVersion === null ||
|
||||||
|
previousTokensVersion === null ||
|
||||||
|
previousUiVersion !== uiVersion ||
|
||||||
|
previousTokensVersion !== tokensVersion;
|
||||||
|
|
||||||
|
return {
|
||||||
|
changed,
|
||||||
|
tag: `cadence-ui-v${uiVersion}`,
|
||||||
|
version: uiVersion
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = parseArgs(process.argv.slice(2));
|
||||||
|
const metadata = getReleaseMetadata(options.before);
|
||||||
|
|
||||||
|
if (options.field) {
|
||||||
|
if (!(options.field in metadata)) {
|
||||||
|
throw new Error(`Unknown field "${options.field}".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(String(metadata[options.field]));
|
||||||
|
} else {
|
||||||
|
process.stdout.write(`${JSON.stringify(metadata, null, 2)}\n`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||||
|
const packagesRoot = path.join(repoRoot, "packages");
|
||||||
|
const tempPrefix = path.join(os.tmpdir(), "cadence-ui-package-consumer-");
|
||||||
|
const keepArtifacts = process.env.CADENCE_KEEP_PACKAGE_SMOKE === "1";
|
||||||
|
const skipBuild = process.env.CADENCE_SKIP_PACKAGE_BUILD === "1";
|
||||||
|
const pnpmCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||||
|
|
||||||
|
function toPosixPath(value) {
|
||||||
|
return value.split(path.sep).join(path.posix.sep);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJson(filePath) {
|
||||||
|
return JSON.parse(await fs.readFile(filePath, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCommand(command, args) {
|
||||||
|
return [command, ...args].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(command, args, options = {}) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
cwd: options.cwd ?? repoRoot,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...(options.env ?? {})
|
||||||
|
},
|
||||||
|
stdio: "inherit"
|
||||||
|
});
|
||||||
|
|
||||||
|
child.once("error", reject);
|
||||||
|
child.once("exit", (code, signal) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`${formatCommand(command, args)} failed with code ${code ?? "null"} and signal ${
|
||||||
|
signal ?? "null"
|
||||||
|
}.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function packPackage(packageDir, outputDir) {
|
||||||
|
const before = new Set(await fs.readdir(outputDir));
|
||||||
|
|
||||||
|
await run(
|
||||||
|
pnpmCommand,
|
||||||
|
["pack", "--pack-destination", outputDir],
|
||||||
|
{
|
||||||
|
cwd: packageDir,
|
||||||
|
env: {
|
||||||
|
CI: "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const after = await fs.readdir(outputDir);
|
||||||
|
const created = after.filter((entry) => !before.has(entry) && entry.endsWith(".tgz"));
|
||||||
|
|
||||||
|
if (created.length !== 1) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected exactly one tarball from ${packageDir}, received ${created.length}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join(outputDir, created[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFileDependency(projectDir, absolutePath) {
|
||||||
|
let relativePath = toPosixPath(path.relative(projectDir, absolutePath));
|
||||||
|
|
||||||
|
if (!relativePath.startsWith(".")) {
|
||||||
|
relativePath = `./${relativePath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `file:${relativePath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeConsumerFixture({
|
||||||
|
projectDir,
|
||||||
|
tokensTarball,
|
||||||
|
uiTarball,
|
||||||
|
rootPackage,
|
||||||
|
uiPackage
|
||||||
|
}) {
|
||||||
|
const packageJson = {
|
||||||
|
name: "cadence-ui-package-consumer-smoke",
|
||||||
|
private: true,
|
||||||
|
type: "module",
|
||||||
|
dependencies: {
|
||||||
|
"@ai-ui/tokens": toFileDependency(projectDir, tokensTarball),
|
||||||
|
"@ai-ui/ui": toFileDependency(projectDir, uiTarball),
|
||||||
|
react: uiPackage.devDependencies.react,
|
||||||
|
"react-dom": uiPackage.devDependencies["react-dom"]
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
"@tailwindcss/vite": rootPackage.devDependencies["@tailwindcss/vite"],
|
||||||
|
"@types/react": rootPackage.devDependencies["@types/react"],
|
||||||
|
"@types/react-dom": rootPackage.devDependencies["@types/react-dom"],
|
||||||
|
tailwindcss: rootPackage.devDependencies.tailwindcss,
|
||||||
|
typescript: rootPackage.devDependencies.typescript,
|
||||||
|
vite: rootPackage.devDependencies.vite
|
||||||
|
},
|
||||||
|
pnpm: {
|
||||||
|
overrides: {
|
||||||
|
"@ai-ui/tokens": toFileDependency(projectDir, tokensTarball)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tsconfig = {
|
||||||
|
compilerOptions: {
|
||||||
|
target: "ES2022",
|
||||||
|
module: "ESNext",
|
||||||
|
moduleResolution: "Bundler",
|
||||||
|
jsx: "react-jsx",
|
||||||
|
strict: true,
|
||||||
|
esModuleInterop: true,
|
||||||
|
skipLibCheck: true,
|
||||||
|
noEmit: true,
|
||||||
|
types: ["react", "react-dom"]
|
||||||
|
},
|
||||||
|
include: ["src", "vite.config.ts"]
|
||||||
|
};
|
||||||
|
|
||||||
|
const viteConfig = `import { defineConfig } from "vite";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tailwindcss()]
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const indexHtml = `<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Cadence UI Package Smoke</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const stylesSource = `@import "tailwindcss";
|
||||||
|
@import "@ai-ui/ui/styles.css";
|
||||||
|
@source "../node_modules/@ai-ui/ui/src";
|
||||||
|
`;
|
||||||
|
|
||||||
|
const mainSource = `import { createRoot } from "react-dom/client";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
Input
|
||||||
|
} from "@ai-ui/ui";
|
||||||
|
import { setTheme } from "@ai-ui/tokens";
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
setTheme("light");
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-[var(--color-background)] p-8 text-[var(--color-foreground)]">
|
||||||
|
<div className="mx-auto grid max-w-xl gap-6">
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<h1 className="text-2xl font-semibold">Cadence UI package smoke</h1>
|
||||||
|
<Input defaultValue="team@cadence.dev" placeholder="Email" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button>Open package smoke dialog</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogTitle>Package consumer</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Verifies published package exports typecheck and build in a consumer app.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = document.getElementById("root");
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
throw new Error("Missing root element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(root).render(<App />);
|
||||||
|
`;
|
||||||
|
|
||||||
|
await fs.mkdir(path.join(projectDir, "src"), { recursive: true });
|
||||||
|
await fs.writeFile(path.join(projectDir, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||||
|
await fs.writeFile(path.join(projectDir, "tsconfig.json"), `${JSON.stringify(tsconfig, null, 2)}\n`);
|
||||||
|
await fs.writeFile(path.join(projectDir, "vite.config.ts"), viteConfig);
|
||||||
|
await fs.writeFile(path.join(projectDir, "index.html"), indexHtml);
|
||||||
|
await fs.writeFile(path.join(projectDir, "src", "global.d.ts"), 'declare module "*.css";\n');
|
||||||
|
await fs.writeFile(path.join(projectDir, "src", "styles.css"), stylesSource);
|
||||||
|
await fs.writeFile(path.join(projectDir, "src", "main.tsx"), mainSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const [rootPackage, uiPackage, tokensPackage] = await Promise.all([
|
||||||
|
readJson(path.join(repoRoot, "package.json")),
|
||||||
|
readJson(path.join(packagesRoot, "ui", "package.json")),
|
||||||
|
readJson(path.join(packagesRoot, "tokens", "package.json"))
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (uiPackage.version !== tokensPackage.version) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected @ai-ui/ui and @ai-ui/tokens to share a version. Received ${uiPackage.version} and ${tokensPackage.version}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectDir = await fs.mkdtemp(tempPrefix);
|
||||||
|
const packDir = path.join(projectDir, "packed");
|
||||||
|
let succeeded = false;
|
||||||
|
|
||||||
|
const cleanup = async () => {
|
||||||
|
if (!succeeded || keepArtifacts) {
|
||||||
|
console.log(`Keeping package smoke fixture at ${projectDir}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.rm(projectDir, { force: true, recursive: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!skipBuild) {
|
||||||
|
console.log("Building workspace packages for package smoke");
|
||||||
|
await run(pnpmCommand, ["build"], {
|
||||||
|
cwd: repoRoot,
|
||||||
|
env: {
|
||||||
|
CI: "1"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.mkdir(packDir, { recursive: true });
|
||||||
|
|
||||||
|
console.log("Packing @ai-ui/tokens");
|
||||||
|
const tokensTarball = await packPackage(path.join(packagesRoot, "tokens"), packDir);
|
||||||
|
|
||||||
|
console.log("Packing @ai-ui/ui");
|
||||||
|
const uiTarball = await packPackage(path.join(packagesRoot, "ui"), packDir);
|
||||||
|
|
||||||
|
console.log(`Creating package consumer fixture in ${projectDir}`);
|
||||||
|
await writeConsumerFixture({
|
||||||
|
projectDir,
|
||||||
|
tokensTarball,
|
||||||
|
uiTarball,
|
||||||
|
rootPackage,
|
||||||
|
uiPackage
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Installing package consumer dependencies");
|
||||||
|
await run(pnpmCommand, ["install", "--ignore-workspace"], {
|
||||||
|
cwd: projectDir,
|
||||||
|
env: {
|
||||||
|
CI: "1"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Verifying CommonJS package entrypoints");
|
||||||
|
await run(process.execPath, ["-e", 'const ui=require("@ai-ui/ui"); const tokens=require("@ai-ui/tokens"); if(!ui.Button||!tokens.setTheme){throw new Error("Missing CommonJS export.");}'], {
|
||||||
|
cwd: projectDir
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Verifying ESM package entrypoints");
|
||||||
|
await run(process.execPath, ["--input-type=module", "-e", 'const ui=await import("@ai-ui/ui"); const tokens=await import("@ai-ui/tokens"); if(!ui.Button||!tokens.setTheme){throw new Error("Missing ESM export.");}'], {
|
||||||
|
cwd: projectDir
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Typechecking the package consumer fixture");
|
||||||
|
await run(pnpmCommand, ["exec", "tsc", "-p", "tsconfig.json", "--noEmit"], {
|
||||||
|
cwd: projectDir
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Building the package consumer fixture");
|
||||||
|
await run(pnpmCommand, ["exec", "vite", "build"], {
|
||||||
|
cwd: projectDir,
|
||||||
|
env: {
|
||||||
|
CI: "1"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
succeeded = true;
|
||||||
|
console.log("Package consumer smoke test passed.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Package consumer smoke test failed. Fixture preserved at ${projectDir}`);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
Reference in New Issue
Block a user