Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
push:
branches: [main]

permissions:
contents: read

concurrency:
group: ci-${{ github.head_ref || github.ref_name }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
Expand Down Expand Up @@ -63,6 +66,25 @@ jobs:
- name: Verify component previews resolve to real stories
run: pnpm -F @vllnt/ui-registry registry:verify-previews

native:
name: Native Package Gates
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v5

- uses: pnpm/action-setup@v5

- uses: actions/setup-node@v5
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Verify native packages and Expo bundles
run: pnpm ci:native

e2e:
name: E2E (Playwright)
runs-on: ubuntu-latest
Expand Down
272 changes: 272 additions & 0 deletions .github/workflows/native-canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
name: Native Canary

on:
push:
branches: [main]
paths:
- "packages/design/**"
- "packages/ui-core/**"
- "packages/ui-native/**"
- "apps/native-catalog/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- "turbo.json"
- ".github/workflows/native-canary.yml"

concurrency:
group: native-canary-main
cancel-in-progress: false

permissions:
contents: read

jobs:
quality:
name: Native Quality Gates
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v5

- uses: pnpm/action-setup@v5

- uses: actions/setup-node@v5
with:
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile

- run: pnpm ci:native

publish:
name: Publish Native Canary Pair
if: vars.NATIVE_CANARY_PUBLISH_ENABLED == 'true'
needs: quality
runs-on: ubuntu-latest
timeout-minutes: 20
environment: npm-native-canary
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v5

- uses: pnpm/action-setup@v5

- uses: actions/setup-node@v5
with:
node-version: 22
cache: pnpm
registry-url: https://registry.npmjs.org

- run: pnpm install --frozen-lockfile

- name: Build publication artifacts
run: pnpm --filter '@vllnt/ui-native...' build

- name: Publish synchronized canaries
shell: bash
run: |
set -euo pipefail

CORE_DIR="packages/ui-core"
NATIVE_DIR="packages/ui-native"

test "$(node -p "require('./${CORE_DIR}/package.json').name")" = "@vllnt/ui-core"
test "$(node -p "require('./${NATIVE_DIR}/package.json').name")" = "@vllnt/ui-native"

CORE_BASE="$(node -p "require('./${CORE_DIR}/package.json').version")"
NATIVE_BASE="$(node -p "require('./${NATIVE_DIR}/package.json').version")"
test "$CORE_BASE" = "$NATIVE_BASE"
case "$CORE_BASE" in
*-*) echo "::error::Base package versions must not be prereleases"; exit 1 ;;
esac

current_main_sha() {
git ls-remote --exit-code origin refs/heads/main | awk '{print $1}'
}
REMOTE_MAIN_SHA="$(current_main_sha)"
test -n "$REMOTE_MAIN_SHA"
if [[ "$REMOTE_MAIN_SHA" != "$GITHUB_SHA" ]]; then
echo "::notice::Skipping superseded canary run for ${GITHUB_SHA}"
exit 0
fi

SHORT_SHA="$(printf '%s' "$GITHUB_SHA" | cut -c1-12)"
CANARY_VERSION="${CORE_BASE}-canary.${GITHUB_RUN_NUMBER}.sha${SHORT_SHA}"
STAGING_TAG="run-${GITHUB_RUN_ID}"

read_tags() {
local package="$1"
local output_file error_file output
output_file="$(mktemp "$RUNNER_TEMP/npm-view-output.XXXXXX")"
error_file="$(mktemp "$RUNNER_TEMP/npm-view-error.XXXXXX")"
if npm view "$package" dist-tags --json >"$output_file" 2>"$error_file"; then
output="$(cat "$output_file")"
rm -f "$output_file" "$error_file"
[[ -n "$output" ]] || output='{}'
if ! jq -e 'type == "object"' <<<"$output" >/dev/null; then
echo "::error::npm returned invalid dist-tag data for ${package}" >&2
return 1
fi
printf '%s\n' "$output"
return 0
fi
if grep -q 'E404' "$output_file" "$error_file"; then
rm -f "$output_file" "$error_file"
printf '{}\n'
return 0
fi
cat "$output_file" "$error_file" >&2
rm -f "$output_file" "$error_file"
return 1
}
tag_value() {
jq -r --arg tag "$2" '.[$tag] // ""' <<<"$1"
}

CORE_TAGS_BEFORE="$(read_tags @vllnt/ui-core)"
NATIVE_TAGS_BEFORE="$(read_tags @vllnt/ui-native)"
CORE_LATEST_BEFORE="$(tag_value "$CORE_TAGS_BEFORE" latest)"
NATIVE_LATEST_BEFORE="$(tag_value "$NATIVE_TAGS_BEFORE" latest)"
CORE_CANARY_BEFORE="$(tag_value "$CORE_TAGS_BEFORE" canary)"
NATIVE_CANARY_BEFORE="$(tag_value "$NATIVE_TAGS_BEFORE" canary)"

npm version "$CANARY_VERSION" --prefix "$CORE_DIR" --no-git-tag-version --ignore-scripts
npm version "$CANARY_VERSION" --prefix "$NATIVE_DIR" --no-git-tag-version --ignore-scripts
node - "$NATIVE_DIR/package.json" "$CANARY_VERSION" <<'NODE'
const fs = require("node:fs");
const [manifestPath, version] = process.argv.slice(2);
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
manifest.dependencies["@vllnt/ui-core"] = version;
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
NODE

PACK_DIR="$RUNNER_TEMP/native-packs"
mkdir -p "$PACK_DIR"
CORE_TARBALL="$(pnpm --dir "$CORE_DIR" pack --pack-destination "$PACK_DIR" | tail -n1)"
NATIVE_TARBALL="$(pnpm --dir "$NATIVE_DIR" pack --pack-destination "$PACK_DIR" | tail -n1)"

test "$(tar -xOf "$CORE_TARBALL" package/package.json | jq -r '.name')" = "@vllnt/ui-core"
test "$(tar -xOf "$CORE_TARBALL" package/package.json | jq -r '.version')" = "$CANARY_VERSION"
test "$(tar -xOf "$NATIVE_TARBALL" package/package.json | jq -r '.name')" = "@vllnt/ui-native"
test "$(tar -xOf "$NATIVE_TARBALL" package/package.json | jq -r '.version')" = "$CANARY_VERSION"
test "$(tar -xOf "$NATIVE_TARBALL" package/package.json | jq -r '.["react-native"]')" = "./dist/index.js"
tar -tf "$NATIVE_TARBALL" | grep -qx 'package/dist/index.js'
test "$(tar -xOf "$NATIVE_TARBALL" package/package.json | jq -r '.dependencies["@vllnt/ui-core"]')" = "$CANARY_VERSION"
if tar -xOf "$NATIVE_TARBALL" package/package.json | grep -q 'workspace:'; then
echo "::error::Packed native manifest still contains a workspace protocol"
exit 1
fi

sed -i '/_authToken/d' "$NPM_CONFIG_USERCONFIG"
unset NODE_AUTH_TOKEN

PROMOTION_STARTED=false
remove_owned_tag() {
local package="$1"
local tag="$2"
local expected="$3"
local tags current
tags="$(read_tags "$package")" || return 1
current="$(tag_value "$tags" "$tag")"
[[ -n "$current" ]] || return 0
if [[ "$current" != "$expected" ]]; then
echo "::error::Refusing to remove ${package} tag ${tag}: expected ${expected}, found ${current}"
return 1
fi
npx --yes npm@11.18.0 dist-tag rm "$package" "$tag" || return 1
tags="$(read_tags "$package")" || return 1
[[ -z "$(tag_value "$tags" "$tag")" ]]
}
cleanup_staging() {
local failed=0
remove_owned_tag @vllnt/ui-core "$STAGING_TAG" "$CANARY_VERSION" || failed=1
remove_owned_tag @vllnt/ui-native "$STAGING_TAG" "$CANARY_VERSION" || failed=1
return "$failed"
}
on_exit() {
local status=$?
local recovery_failed=0
trap - EXIT
if [[ $status -ne 0 && "$PROMOTION_STARTED" = true ]]; then
restore_pair || recovery_failed=1
fi
cleanup_staging || recovery_failed=1
if [[ $recovery_failed -ne 0 ]]; then
echo "::error::Could not fully restore npm dist-tags"
[[ $status -ne 0 ]] || status=1
fi
exit "$status"
}
trap on_exit EXIT

if ! npm view "@vllnt/ui-core@${CANARY_VERSION}" version >/dev/null 2>&1; then
npx --yes npm@11.18.0 publish "$CORE_TARBALL" --tag "$STAGING_TAG" --provenance --access public
fi

for attempt in $(seq 1 12); do
npm view "@vllnt/ui-core@${CANARY_VERSION}" version >/dev/null 2>&1 && break
sleep 5
done
npm view "@vllnt/ui-core@${CANARY_VERSION}" version >/dev/null

if ! npm view "@vllnt/ui-native@${CANARY_VERSION}" version >/dev/null 2>&1; then
npx --yes npm@11.18.0 publish "$NATIVE_TARBALL" --tag "$STAGING_TAG" --provenance --access public
fi

for attempt in $(seq 1 12); do
npm view "@vllnt/ui-native@${CANARY_VERSION}" version >/dev/null 2>&1 && break
sleep 5
done
npm view "@vllnt/ui-native@${CANARY_VERSION}" version >/dev/null

REMOTE_MAIN_SHA="$(current_main_sha)"
test -n "$REMOTE_MAIN_SHA"
if [[ "$REMOTE_MAIN_SHA" != "$GITHUB_SHA" ]]; then
echo "::notice::Skipping canary promotion because main advanced to ${REMOTE_MAIN_SHA}"
exit 0
fi

restore_tag() {
local package="$1"
local previous="$2"
local tags current
tags="$(read_tags "$package")" || return 1
current="$(tag_value "$tags" canary)"
[[ "$current" = "$previous" ]] && return 0
if [[ -n "$current" && "$current" != "$CANARY_VERSION" ]]; then
echo "::error::Refusing to overwrite unexpected ${package} canary ${current}"
return 1
fi
if [[ -n "$previous" ]]; then
npx --yes npm@11.18.0 dist-tag add "${package}@${previous}" canary || return 1
else
remove_owned_tag "$package" canary "$CANARY_VERSION" || return 1
fi
tags="$(read_tags "$package")" || return 1
[[ "$(tag_value "$tags" canary)" = "$previous" ]]
}

restore_pair() {
local failed=0
restore_tag @vllnt/ui-core "$CORE_CANARY_BEFORE" || failed=1
restore_tag @vllnt/ui-native "$NATIVE_CANARY_BEFORE" || failed=1
return "$failed"
}

PROMOTION_STARTED=true
npx --yes npm@11.18.0 dist-tag add "@vllnt/ui-core@${CANARY_VERSION}" canary
npx --yes npm@11.18.0 dist-tag add "@vllnt/ui-native@${CANARY_VERSION}" canary

CORE_TAGS_AFTER="$(read_tags @vllnt/ui-core)"
NATIVE_TAGS_AFTER="$(read_tags @vllnt/ui-native)"
test "$(tag_value "$CORE_TAGS_AFTER" canary)" = "$CANARY_VERSION"
test "$(tag_value "$NATIVE_TAGS_AFTER" canary)" = "$CANARY_VERSION"
test "$(tag_value "$CORE_TAGS_AFTER" latest)" = "$CORE_LATEST_BEFORE"
test "$(tag_value "$NATIVE_TAGS_AFTER" latest)" = "$NATIVE_LATEST_BEFORE"

PROMOTION_STARTED=false
exit 0
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ node_modules/

# Build outputs
.next/
.expo/
dist/
.turbo/
coverage/
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Release automation can regenerate this file from Conventional Commits with

### Added

- **Cross-platform foundation** — added the framework-free `@vllnt/ui-core` token/contract package, an experimental source-only `@vllnt/ui-native` renderer with 171 foundation, form, data, content, AI, learning, motion, utility, control, overlay, and navigation modules, plus a private Expo catalog for Android/iOS Metro validation. The URL-driven registry UI, dedicated native hub, component pages, search, llms surfaces, JSON-LD, native manifest, and MCP expose truthful web/native availability. No native npm release exists yet; existing `@vllnt/ui` exports and stable publishing remain unchanged. (#479)
- **Component family landing pages** - every component family has a standalone,
SEO-oriented landing at `/families/[category]`, plus a `/families` index. One
shared template renders a hero with CTAs, per-family SEO sub-groups with
Expand Down
33 changes: 24 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Key scripts (from repo root):
| `pnpm -F @vllnt/ui test:visual` | Playwright CT visual snapshots |
| `pnpm check:circular` | Fail on circular imports |
| `pnpm doctor` | react-doctor React-health scan |
| `pnpm tokens:check` | Verify generated token artifacts |
| `pnpm ci:native` | Verify core/native packages and Expo bundles |

A [react-doctor](https://github.com/millionco/react-doctor) **pre-commit hook**
(in `.githooks/`, enabled automatically on `pnpm install`) blocks commits that
Expand Down Expand Up @@ -58,7 +60,7 @@ with `git commit --no-verify`. See AGENTS.md → *React health* for details.
```

2. Follow the existing patterns:
- `React.forwardRef` on every component.
- React 19 ref-as-prop support and `displayName` on every named component.
- `cn()` from `src/lib/utils.ts` for class merging.
- Radix primitives for accessible behavior where applicable.
- CVA for variants (`class-variance-authority`).
Expand All @@ -78,6 +80,25 @@ with `git commit --no-verify`. See AGENTS.md → *React health* for details.
pnpm lint && pnpm test:once && pnpm -F @vllnt/ui test:visual && pnpm build
```

For native changes, also run:

```bash
pnpm -F @vllnt/ui-native generate:index:check
pnpm -F @vllnt/ui-native boundaries:check
pnpm -F @vllnt/ui-native pack:check
pnpm ci:native
```

## Adding a native component

1. Add `packages/ui-native/src/components/{name}/{name}.tsx` using React Native core primitives, semantic theme tokens, native accessibility APIs, controlled/uncontrolled state where applicable, and caller-owned selection IDs.
2. Do not import DOM, Radix, Tailwind, NativeWind, or browser globals. Inject capabilities such as clipboard and file selection when React Native core does not provide a portable service.
3. Add the component to `packages/ui-native/registry.json` in alphabetical order with honest `portable-options` or `native-adapted` compatibility and its native source path.
4. Run `pnpm -F @vllnt/ui-native generate:index`; never hand-maintain the generated barrel.
5. Add interaction/accessibility tests and run the native checks listed below. Update the Expo catalog when the new family needs integration proof.

Native remains source-only until the manifest reports package availability. Do not describe the planned canary command as installable before publication.

## Code style

- TypeScript **strict** via `@vllnt/typescript`.
Expand All @@ -87,15 +108,9 @@ with `git commit --no-verify`. See AGENTS.md → *React health* for details.

## Releases

Releases are cut via `workflow_dispatch` on `.github/workflows/publish.yml`. Maintainers pick `patch` / `minor` / `major` and the workflow:

1. Bumps `packages/ui/package.json`.
2. Generates release notes from commits.
3. Pushes an annotated tag `v{x.y.z}` back to `main`.
4. Publishes to the public npm registry with OIDC-signed provenance.
5. Creates the GitHub release.
Stable `@vllnt/ui` versions are prepared in a normal version-bump PR. A maintainer then dispatches `.github/workflows/publish.yml` from `main`; the workflow validates the pre-bumped version, publishes with OIDC-signed provenance, tags it, and creates the GitHub release. Web canaries publish automatically after pushes to `main`.

Canary builds ship automatically on every push to `main`.
`@vllnt/ui-core` and `@vllnt/ui-native` are experimental. `.github/workflows/native-canary.yml` publishes them as a synchronized pair only on the `canary` tag. It has no manual dispatch, stable tag, Git tag, or GitHub Release path. Enabling a stable native release requires a separate reviewed workflow change. See [docs/RELEASING.md](docs/RELEASING.md).

## Reporting bugs / requesting features

Expand Down
Loading
Loading