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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/frontend-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile --filter=testing-view --filter=flashing-view --filter=competition-view --filter=logging-view --filter=adj-view --filter=ui --filter=core
run: pnpm install --frozen-lockfile --filter="./frontend/**"

- name: Build frontend
run: pnpm build --filter="./frontend/**"
Expand Down
202 changes: 202 additions & 0 deletions .github/workflows/publish-frontend-kit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
name: Publish Frontend Kit

# Publishes @hyperloop-upv/frontend-kit to GitHub Packages so a frontend in
# another repository can install it.
#
# Normally runs as part of release.yaml, at the same version as the Control
# Station, so the two always ship together. Can also be dispatched on its own
# to cut a kit-only release.
#
# As with electron-app, the version is injected here and is not committed: the
# number in package.json is a placeholder, and the git tag created by
# release.yaml is the record of what was published from which commit.
#
# A published version can never be reused. Deleting it does not free the
# number: if 11.3.2 is wrong, publish 11.3.3.

on:
workflow_call:
inputs:
version:
description: "Version to publish, without a leading v"
required: true
type: string
dry-run:
description: "Pack and check only, do not publish"
type: boolean
default: true
tag:
description: "npm dist-tag. Derived from the version when empty"
type: string
default: ""
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g., 11.3.2)"
required: true
dry-run:
description: "Pack and check only, do not publish"
type: boolean
default: true
tag:
description: "npm dist-tag. Leave empty to derive it from the version"
default: ""

jobs:
publish:
name: Publish @hyperloop-upv/frontend-kit
runs-on: ubuntu-latest
permissions:
contents: read
# Not the default. Without it, publishing fails with a bare 403.
packages: write
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 10.26.0

- uses: actions/setup-node@v4
with:
node-version: "24"
cache: "pnpm"
# Writes an auth-ready npmrc outside the working tree, so the git
# tree stays clean.
registry-url: "https://npm.pkg.github.com"
scope: "@hyperloop-upv"

- name: Resolve version and dist-tag
id: resolve
shell: bash
run: |
set -euo pipefail

# release.yaml tags as v<version>, so accept either spelling.
version="${{ inputs.version }}"
version="${version#v}"

if [ -z "$version" ]; then
echo "::error::version is empty"
exit 1
fi

requested="${{ inputs.tag }}"
if [ -n "$requested" ]; then
tag="$requested"
elif [ "${version#*-}" != "$version" ]; then
# A prerelease such as 11.3.2-beta-2 must not land on "latest", or
# it becomes the default install for every consumer.
tag="next"
else
tag="latest"
fi

echo "version=$version" >> $GITHUB_OUTPUT
echo "tag=$tag" >> $GITHUB_OUTPUT
echo "publishing $version under dist-tag $tag"

- name: Install dependencies
run: pnpm install --frozen-lockfile

# Same approach as electron-app in release.yaml: set it here, do not
# expect it committed.
- name: Set the version
working-directory: frontend/frontend-kit/kit
run: pnpm version ${{ steps.resolve.outputs.version }} --no-git-tag-version --allow-same-version

- name: Build
run: pnpm build --filter=@hyperloop-upv/frontend-kit

- name: Pack
shell: bash
run: |
set -euo pipefail
mkdir -p /tmp/kit
pnpm --filter @hyperloop-upv/frontend-kit exec pnpm pack --pack-destination /tmp/kit
echo "::group::Tarball contents"
tar -tzf /tmp/kit/hyperloop-upv-frontend-kit-${{ steps.resolve.outputs.version }}.tgz | sort
echo "::endgroup::"

# Catches the failures that are cheap to prevent and impossible to undo
# once a version is published.
- name: Check the tarball
shell: bash
run: |
set -euo pipefail
T=/tmp/kit/hyperloop-upv-frontend-kit-${{ steps.resolve.outputs.version }}.tgz

# Via a file, not a pipe: a heredoc would occupy stdin and the
# script would read nothing.
tar -xzOf "$T" package/package.json > /tmp/kit/packed.json

EXPECTED_VERSION="${{ steps.resolve.outputs.version }}" python3 - <<'PY'
import json, os, sys

p = json.load(open("/tmp/kit/packed.json"))
failures = []

def check(cond, msg):
print((" ok " if cond else " FAIL ") + msg)
if not cond:
failures.append(msg)

check(
p["version"] == os.environ["EXPECTED_VERSION"],
f"version is {os.environ['EXPECTED_VERSION']}, got {p['version']}",
)
check(not p.get("private"), "private:true does not survive")
check("preinstall" not in p.get("scripts", {}), "the preinstall hook does not survive")
check(
p["exports"]["."]["default"].startswith("./dist"),
"publishConfig.exports was applied (npm publish would skip it)",
)
check(p["exports"]["./core"]["default"] == "./dist/core/index.js", "./core points at dist")
check(
p["exports"]["./globals.css"] == "./src/styles/globals.css",
"globals.css is served from the shipped src, so Tailwind can scan it",
)
check(
set(p.get("peerDependencies", {}))
>= {"react", "react-dom", "lucide-react", "tailwindcss", "tw-animate-css"},
"the five peer dependencies are declared",
)
check(
not any(str(v).startswith("workspace:") for v in p.get("dependencies", {}).values()),
"no literal workspace: range leaked into the tarball",
)

if failures:
sys.exit("\nRefusing to publish: " + "; ".join(failures))
print("\ntarball ok")
PY

# Tailwind needs the kit's sources to find its classes in node_modules.
tar -tzf "$T" | grep -qx 'package/src/components/shadcn/sidebar.tsx' \
|| { echo "::error::src/ is missing from the tarball"; exit 1; }
echo " ok src/ ships alongside dist/"

- uses: actions/upload-artifact@v4
with:
name: frontend-kit-${{ steps.resolve.outputs.version }}
path: /tmp/kit/*.tgz
retention-days: 7

# Fails closed: anything other than an explicit boolean false skips this.
- name: Publish
if: ${{ inputs.dry-run == false }}
run: pnpm publish --filter=@hyperloop-upv/frontend-kit --no-git-checks --tag ${{ steps.resolve.outputs.tag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Summary
shell: bash
run: |
V='${{ steps.resolve.outputs.version }}'
if [ '${{ inputs.dry-run }}' = 'true' ]; then
echo "### Dry run: nothing was published" >> $GITHUB_STEP_SUMMARY
echo "Tarball for \`$V\` is attached as an artifact. Re-run with dry-run off to publish." >> $GITHUB_STEP_SUMMARY
else
echo "### Published \`@hyperloop-upv/frontend-kit@$V\`" >> $GITHUB_STEP_SUMMARY
echo "dist-tag: \`${{ steps.resolve.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
fi
21 changes: 21 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ on:
description: "Include ADJ Viewer"
type: boolean
default: true
publish-kit:
description: "Publish @hyperloop-upv/frontend-kit at this same version"
type: boolean
default: true


jobs:
Expand All @@ -44,6 +48,7 @@ jobs:
include_flashing: ${{ steps.get_version.outputs.include_flashing }}
include_logging: ${{ steps.get_version.outputs.include_logging }}
include_adj: ${{ steps.get_version.outputs.include_adj }}
publish_kit: ${{ steps.get_version.outputs.publish_kit }}
steps:
- name: Determine version
id: get_version
Expand All @@ -55,6 +60,7 @@ jobs:
echo "include_flashing=${{ github.event.inputs.include-flashing }}" >> $GITHUB_OUTPUT
echo "include_logging=${{ github.event.inputs.include-logging }}" >> $GITHUB_OUTPUT
echo "include_adj=${{ github.event.inputs.include-adj }}" >> $GITHUB_OUTPUT
echo "publish_kit=${{ github.event.inputs.publish-kit }}" >> $GITHUB_OUTPUT

create-draft-release:
name: Create Draft Release
Expand Down Expand Up @@ -380,3 +386,18 @@ jobs:
| xargs gh release upload v${{ needs.determine-version.outputs.version }} --clobber
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

publish-frontend-kit:
name: Publish Frontend Kit
# Last, and only once the Control Station release itself succeeded:
# publishing to the registry cannot be undone, so it must not happen for a
# release that failed to build.
needs: [determine-version, package-and-upload]
if: needs.determine-version.outputs.publish_kit == 'true'
permissions:
contents: read
packages: write
uses: ./.github/workflows/publish-frontend-kit.yaml
with:
version: ${{ needs.determine-version.outputs.version }}
dry-run: false
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ node_modules/
# Global binaries
*.exe

# Build output
dist/

# electron modules
electron-app/python
electron-app/logger
11 changes: 11 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
; Where to look for the @hyperloop-upv scope. Needed so that `pnpm publish`
; targets GitHub Packages instead of npmjs.com.
;
; Deliberately no auth token line here: every @hyperloop-upv/* dependency in
; this monorepo is a workspace link, so pnpm never contacts the registry and
; nobody needs a token to install. Adding _authToken=${NODE_AUTH_TOKEN} would
; make `pnpm install` fail for anyone without that variable set.
;
; That holds only while no @hyperloop-upv/* dependency here comes from the
; registry. If one ever does, auth becomes mandatory for all contributors.
@hyperloop-upv:registry=https://npm.pkg.github.com
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ Our `pnpm-workspace.yaml` defines the following workspaces:
| `blcu-programming` | Python | FastAPI service that flashes firmware to the BLCU board via TFTP |
| `hyperloop-control-station` | JS | The main Control Station electron desktop application |
| `e2e` | TS | End-to-end tests for the whole app (Playwright) |
| `@workspace/ui` | TS/React | Shared UI component library (frontend-kit) |
| `@workspace/core` | TS | Shared business logic and types (frontend-kit) |
| `@workspace/eslint-config` | ESLint | Common ESLint configuration (frontend-kit) |
| `@workspace/typescript-config` | TS | Common TypeScript configuration (frontend-kit) |
| `@hyperloop-upv/frontend-kit` | TS/React | Shared UI components, hooks, icons and the WebSocket layer (its `./core` subpath) |
| `@hyperloop-upv/eslint-config` | ESLint | Common ESLint configuration (frontend-kit) |
| `@hyperloop-upv/typescript-config` | TS | Common TypeScript configuration (frontend-kit) |

---

Expand Down Expand Up @@ -82,4 +81,4 @@ All Turbo scripts support filtering to target specific workspaces:

- `pnpm ui:add <component-name>` - To add shadcn/ui components

> Note: don't forget to also include it in frontend-kit/ui/src/components/shadcn/index.ts to be able to access it from @workspace/ui
> Note: don't forget to also include it in frontend-kit/kit/src/components/shadcn/index.ts to be able to access it from @hyperloop-upv/frontend-kit
25 changes: 12 additions & 13 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ The frontend is organized as 7 workspaces out of 10 in the whole monorepo, divid
| `testing-view` | Primary telemetry testing and debugging interface |
| `competition-view` | Competition-focused UI (simplified view) |
| `flashing-view` | UI for flashing firmware to the BLCU board |
| `frontend-kit/ui` or `@workspace/ui` | Component library built on shadcn/ui and Radix UI |
| `frontend-kit/core` or `@workspace/core` | Shared business logic, WebSocket utilities, and types |
| `frontend-kit/eslint-config` or `@workspace/eslint-config` | Common ESLint configurations |
| `frontend-kit/typescript-config` or `@workspace/typescript-config` | Common TypeScript configurations |
| `frontend-kit/kit` or `@hyperloop-upv/frontend-kit` | Component library built on shadcn/ui and Radix UI, plus the framework-agnostic `./core` subpath (WebSocket, logging, types) |
| `frontend-kit/eslint-config` or `@hyperloop-upv/eslint-config` | Common ESLint configurations |
| `frontend-kit/typescript-config` or `@hyperloop-upv/typescript-config` | Common TypeScript configurations |

## Key Technologies

Expand Down Expand Up @@ -66,7 +65,7 @@ Testing View supports multiple workspaces to organize different testing scenario

The frontend connects to the Go backend via WebSocket for real-time communication:

- **Connection**: `useWebSocket` hook from `@workspace/core`
- **Connection**: `useWebSocket` hook from `@hyperloop-upv/frontend-kit/core`
- **Topics**: Subscribe to specific data streams using `useTopic`
- `podData/update` - Telemetry data
- `connection/update` - Backend connection status
Expand All @@ -76,8 +75,8 @@ The frontend connects to the Go backend via WebSocket for real-time communicatio
Example:

```tsx
import { useTopic, useWebSocket } from "@workspace/ui/hooks";
import { socketService } from "@workspace/core";
import { useTopic, useWebSocket } from "@hyperloop-upv/frontend-kit/hooks";
import { socketService } from "@hyperloop-upv/frontend-kit/core";

const { isConnected } = useWebSocket();

Expand All @@ -88,7 +87,7 @@ useTopic<TelemetryData>("podData/update", (data) => {
socketService.post("order/send", <payload>);
```

## Component Library (@workspace/ui)
## Component Library (@hyperloop-upv/frontend-kit)

The shared UI package provides:

Expand All @@ -97,11 +96,11 @@ The shared UI package provides:
- Hooks (useWebSocket, useTopic, useLogger)
- Icons from Lucide React

Import components from `@workspace/ui`:
Import components from `@hyperloop-upv/frontend-kit`:

```tsx
import { Button, Dialog } from "@workspace/ui";
import { Plus, Settings } from "@workspace/ui/icons";
import { Button, Dialog } from "@hyperloop-upv/frontend-kit";
import { Plus, Settings } from "@hyperloop-upv/frontend-kit/icons";
```

## Development Patterns
Expand All @@ -119,14 +118,14 @@ To add a new Lucide icon to the shared UI library:

1. Look up the icon on [Lucide Icons](https://lucide.dev/icons)
2. Find the **first category** listed for that icon
3. Add the import to the corresponding category file in `frontend-kit/ui/src/icons/`
3. Add the import to the corresponding category file in `frontend-kit/kit/src/icons/`
4. If the category file doesn't exist, create it and add its export to `index.ts`
5. Keep the same alphabetical order as the icon categories

**Example:** The `Axe` icon's first category is `Tools`, so you would add its import to `tools.ts`:

```js
// frontend-kit/ui/src/icons/tools.ts
// frontend-kit/kit/src/icons/tools.ts
export { Axe, Hammer } from "lucide-react";
```

Expand Down
Loading
Loading