Beamline

Docs · Updated

Upgrade Beamline

Part of the documentation that comes with Beamline's complete system (docs/UPGRADE.md in the kit). Your coding agent can do all of this for you through Beamline's MCP server: connect it.

Read the release's migration notes before moving to another minor version. The additive-only 0.1.x promise remains: props may be added within 0.1, while the coordinated 0.2 migration can change props, roots/slots, CSS and appearance contracts. Release descriptors identify exact immutable graph, source and package artifacts.

To 0.2.0-rc.22: settings with navigation none sit in your page

A <Settings navigation="none"> no longer paints its own background or centres its column: it takes your page's background and starts at its left edge, so give its container the padding your other content has. The profile section's help text is neutral now; tests that matched the old sentences need the new ones.

To 0.2.0-rc.21: a data grid fits its rows; required fields name themselves once

  • DataGrid height is now the most the grid grows to: a grid with a few rows is as tall as its rows, so the content under it moves up. To keep a fixed box, give the grid's container that height and pass height="fill".
  • Tests that found a required field by its old name ("Email required") now find it by the label's words ("Email"); the field still reports required. No visual change.

To 0.2.0-rc.20: looks bring their fonts

The next install adds your looks' typefaces as packages (Prism: Geist, Geist Mono, Instrument Serif; the stylesheet imports them), so type that fell back to the system face now shows the look's own. Each face loads only where text uses it. To keep a font out, remove --fonts <id>; Prism's parts also changed their finish (see the changelog), with no prop or layout changes.

To 0.2.0-rc.19: Prism becomes the default look

Your app keeps the look it installed when its root names it: <UIRoot preset={soft}> (or data-look="soft" on a static root) looks exactly as before. Three cases change:

  • A root that names no look. A <UIRoot> without preset, or <html class="pui dark"> without data-look, now means Prism. If the app installed only Soft (the old default), that root is no longer painted: switches, slider tracks and field frames go invisible. Name the look you installed (preset={soft} from @beamline/look-soft, or data-look="soft"), or add Prism with install --preset prism. The audit flags the UIRoot case.
  • Pop without an accent. UIRoot no longer applies the blue accent when none is asked for, so Pop's own tomato now shows. Pass accent="blue" (or a brand) to keep blue.
  • Choice controls (since rc.15): segmented control, chip group and radio cards show their label like every field; a view switch or a quick filter passes hideLabel.

Managed 0.2 source

node /path/to/release/tools/beamline.mjs upgrade \
  --release /path/to/new-release/release.json --app /path/to/app

The tool uses the existing destination (default src/ui/beamline; repeat a custom --to). It fetches only declared selected files, verifies their hashes and stages a whole-selection transaction before applying. Add --stage-only to inspect before applying, or --dry-run for a preview that leaves the app unchanged. Only explicit upgrade changes the pinned release; adding/removing roots stays at the accepted release.

Every managed file has three byte records: B, the accepted transformed upstream baseline; L, current local bytes or deletion; U, the target transformed upstream bytes or deletion. Saved hashes point to retained bytes under .beamline/blobs; hashes alone are never treated as baselines.

Observed state Outcome
Local bytes equal target Accept the target baseline without rewriting the file
Upstream is unchanged Preserve the local edit or deletion without a sidecar
Local bytes equal accepted baseline Replace or remove from the staged target
Both changed, clean text merge Stage the merged bytes and retain U as the new baseline
Both changed, overlapping/binary/delete-modify conflict Keep the whole live selection unchanged and save a pending proposal
A new path already exists without ownership Report a collision, even when bytes match
A removed helper is still imported by a surviving local edit Retain and report the helper as an orphan

Git enables clean three-way text merging. Without it, both-changed files remain pending for explicit resolution. The tool never silently recreates a local deletion against changed upstream bytes. A modified file removed upstream remains pending removal.

beamline-ui.lock.json reports acceptedRelease independently from pending. Each transaction records per-file accepted/target releases and B/L/U/result hashes. Baseline blobs, source import transforms, transaction state and backups stay in .beamline; retain this directory with the managed source. The accepted release's verified descriptor/graph cache is named by manifestPath in the lock.

Resolve a pending proposal

node /path/to/release/tools/beamline.mjs status --app /path/to/app
node /path/to/release/tools/beamline.mjs resolve components/button/button.tsx \
  --use local --app /path/to/app
node /path/to/release/tools/beamline.mjs apply --app /path/to/app --install

Choose --use upstream to accept the proposed upstream bytes/deletion, or --use file --file /path/to/reviewed-merge.tsx for your merged file. --use local explicitly keeps the current live bytes/deletion while accepting U as the next upstream baseline. Resolving an ownership collision explicitly grants ownership of that path. No live source changes until all conflicts resolve and apply succeeds.

The status JSON exposes baseHash, localHash, upstreamHash and resultHash; open the corresponding .beamline/blobs/<hash> files to compare exact content. A null hash means deletion. Repeating the pending target leaves its transaction id and proposal bytes unchanged. A different target cannot overwrite it. discard abandons an unapplied proposal while retaining evidence; then request a new target.

Changed live bytes after staging prevent apply. Resolve that path explicitly or discard and replan. A conflicting caller holds the selected group, including helper changes. Imported orphan helpers remain marked in the lock until the customer removes the dependency or resolves their ownership.

Recover interruption

Before live writes, the transaction saves original local bytes, proposed results and the previous accepted lock. Each completed file write is recorded. A failed/interrupted apply leaves the accepted release unchanged until all files commit.

node /path/to/release/tools/beamline.mjs recover --rollback --app /path/to/app
# Or finish the already staged transaction:
node /path/to/release/tools/beamline.mjs recover --resume --app /path/to/app

Rollback is the default. Both choices inspect all live files first; if a customer changed a file after interruption, recovery refuses to overwrite it. Preserve that edit outside the managed folder, restore that path to its saved original or proposed bytes, and retry the chosen recovery. A completed source apply can still require a package install: deps --install retries only dependencies from the accepted source descriptor. The installer moves existing direct Beamline packages to matching target artifacts together with the source dependencies. If the target descriptor lacks an installed package, it stops and names that package; use a descriptor containing the existing selection. npm/pnpm lockfiles describe actual package resolution.

Compiled packages and legacy source

Install selected compiled packages from the new descriptor with install <id…> --upgrade --release <target> and rebuild the app. Source/package mixtures must use compatible canonical runtime/foundation packages; upgrade an existing source selection before installing packages from another release.

For a legacy 0.1 package, keep the new tarball under a distinct path and install it with the app's package manager. Never replace bytes at an existing version/digest. For a legacy source lock with hashes only, keep the original bundle and compare original/local/target files manually. The new tool refuses automatic migration without accepted baseline bytes. It never claims a requested target is already accepted.

From rc.4 the first compiled install (any root, a look, or --upgrade) rewrites a package selection made with rc.3 or earlier: the internal Beamline units it had listed in dependencies move to overrides, leaving the chosen packages (a trial app went from 81 entries to 45). Registry packages an earlier installer added (Radix, Motion, recharts, …) stay, because the tool cannot tell them from your own; remove those your code does not import. React is never changed. Then replace the per-component import "@beamline/…/styles.css" lines with the one printed beamline.css import; keeping both works but brings foundation in again.

From rc.5 a compiled install keeps its records and archives in the app's root .beamline/ instead of src/ui/beamline/.beamline/. The first command that writes (install, remove, style add) moves the records; the next install (or deps --install) repoints package.json, the lockfile and pnpm's overrides at the root archives, then deletes the old ones and any folders that leaves empty. An app that also holds editable source keeps that source and its lock where they are. Commit the root .beamline/ with the app. Editable-source state now keeps only what recovery and three-way merges need: every accepted baseline, a pending or interrupted transaction, the latest accepted transaction (its copy of the release lives once, in releases/) and proposals declined since; older transactions, release copies and their blobs are removed after each accept, discard or rollback.

pnpm archive overrides include the exact Beamline version. Installing another release adds its matching archive mappings without redirecting another version in the workspace. Unrelated overrides and workspace settings are preserved. After a package-manager failure, use deps --install for accepted source or repeat the compiled install command; source acceptance and package resolution remain separate results.

0.2 field styling

className and style address the visual field root. Input, PasswordField, SearchField and Textarea previously passed style to their native input; move native-only styles to a class and pass classNames={{ input: "my-input" }}. classNames.frame, label, description and error style the corresponding shared parts. Picker-specific names are declared in their props and metadata. Native values, input attributes and refs retain their native control target; Textarea/SearchField compose the forwarded ref with internal measurement/focus, including callback cleanup. Textarea row bounds use the actual line height, so larger type and input-slot line-height changes keep rows/maxRows consistent.

VoicePicker now applies style to its labelled wrapper; use classNames.control for the inner control row. Labelled Checkbox and Switch also apply style to the labelled wrapper; use classNames.control for the button. Their refs still address the native control. Bare Checkbox and Switch retain the button as their styling root.

Chart style, className and ref address the figure; classNames exposes documented frame, tooltip and slice-list parts where present. Plot measurements exclude customer-added padding. Chart entrances and gauge glides now follow managed motion and host-local timing tokens, without replaying a completed SVG entrance on a look change.

Customer-owned custom styles

style add stores the custom .style.json, .preset.css and appearance.ts outside the managed component tree (default src/styles/beamline). Package/source upgrades preserve them, including local edits. The app's beamline.css beside them imports the selected base look's structural stylesheet; keep that look in the upgraded package selection. Reapplying a style uses the app's accepted release and refuses to overwrite files edited since its last application. To change the style, use its editable source and style validate/style add; a new base look needs its selected package installed.

0.2 chart look resources

Charts no longer initialize all built-in SVG effects on mount. Follow the selected look's runtime setup printed by the CLI (Glow, Mono and Ink currently declare it). Applications offering every built-in look can explicitly call ensureLookFilters() from @beamline/ui/lib/look-filters once and keep its cleanup for teardown. Selected applications import only each selected look's runtime. The reference app and gallery own this setup at their application boundary.