diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ca66670..a36ec35d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,10 +40,6 @@ on: description: "Release everything outstanding since the last tag" type: boolean default: false - registry_smoke: - description: "Smoke-test the currently published packages" - type: boolean - default: false jobs: build-and-test: @@ -115,18 +111,27 @@ jobs: run: npm run test:e2e registry-smoke: - name: Registry smoke test (published packages) + name: Registry smoke test (packed local build) runs-on: ubuntu-latest - # Tests the published packages, not this commit — pointless on a PR build; run by hand after a release. - if: github.event_name == 'workflow_dispatch' && inputs.registry_smoke + needs: build-and-test + # Ticket 823: this used to install from the npm registry at `latest`, gated behind + # workflow_dispatch only — so a source rename here couldn't fail until the next release + # republished, in a job nobody was watching. It now packs jarl-atoms/jarl-react from this + # commit as `npm pack` would for a release and installs those tarballs as a real npm + # dependency (see e2e/registry-smoke/README.md, "Tracking source renames"), so drift shows up + # on the PR that introduced it instead of after a release ships broken. steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: 24 + cache: npm + + - name: Install dependencies + run: npm ci - - name: Install the smoke consumer from the registry + - name: Pack local build and install the smoke consumer run: npm run test:smoke:install - name: Run the smoke consumer diff --git a/CLAUDE.md b/CLAUDE.md index 2b4c743b..ff5c9367 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,14 +14,18 @@ JARL ("JARL: Atomic Routing Library") is a controlled-component router for React dogfoods the two packages above for its own navigation - `e2e/` — Playwright suite plus the minimal Vite fixture app it drives. A separate npm project (not a workspace) with its own deps: `npm run test:e2e:install` first. -- `e2e/registry-smoke/` — a consumer project that installs both packages from the npm - registry and uses them unlinked, so the published tarballs get exercised. Also a - separate npm project; run it after a release, not against working-tree changes. - `cjs-nodenext/` inside it is the exception: it packs and installs from the working - tree, to typecheck a `node16`-resolution CommonJS consumer against uncommitted builds. +- `e2e/registry-smoke/` — a consumer project that installs both packages as a real npm + dependency, unlinked from the workspace — `pack-local.mjs` packs them from the working + tree and installs the tarballs, so it exercises the same `dist`/`exports`/`.d.ts` a + release would ship, on every PR (see its README, "Tracking source renames"). Also a + separate npm project. `cjs-nodenext/` inside it does the same pack-and-install, to + typecheck a `node16`-resolution CommonJS consumer. - `infra/` — AWS CDK app provisioning the hosting for jarl.randomdev.co.uk. Also a separate npm project, kept out of the workspaces so it is never published: `infra/README.md`. +A new atom's name says what it returns: `*RouteAtom` if its value is a `RouteAtom`, `*Atom` +otherwise. See `packages/jarl-atoms/DESIGN-NOTES.md`. + The two packages are deliberately separate import paths: `jarl-react` does **not** re-export `jarl-atoms`. Consumers get route atoms from `jarl-atoms` and the React bindings from `jarl-react`, so the framework boundary stays visible and `jarl-atoms` is usable on its own. diff --git a/README.md b/README.md index 47b288d8..518f5f07 100644 --- a/README.md +++ b/README.md @@ -16,30 +16,30 @@ wanted something that did just this job extremely well, but without getting in t dictating application structure, and without forcing route matching logic into the component tree itself, where it never seemed to belong. JARL builds that mapping out of composable atoms using [jotai](https://jotai.org/) under the hood: each route is its own atom, with a link to a -parent atom and so on up to the [`rootAtom`](/api/jarl-atoms#rootatom); each one matching a -piece of the URL (normally a path segment) and telling you both whether it *currently* matches, +parent atom and so on up to the [`rootRoute`](/api/jarl-atoms#rootroute); each one matching a +piece of the URL (normally a path segment) and telling you both whether it _currently_ matches, as well as **how to build a URL _to_ that route** based on a given state. Routing decisions in your application then decompose to very simple logic based on the current states of these atoms; a simple `switch` statement or series of `if`s is enough to decide what components to -render, and navigation can be performed by *calling the atom setter*. (Convenience components +render, and navigation can be performed by _calling the atom setter_. (Convenience components like [``](/api/jarl-react#route) and [``](/api/jarl-react#switch) and of course the ubiquitous [``](/api/jarl-react#link) are of course provided in the React package, if you want to build more compositionally; they all just accept atoms for parameters instead of type-unsafe strings.) Because each route atom is an independent, subscribable unit of jotai state, a component that -reads one only re-renders when *that atom's* derived value actually changes - it turns out this +reads one only re-renders when _that atom's_ derived value actually changes - it turns out this is incredibly efficient. ## Features -* Map URLs directly to state (and back again) - the URL becomes the source of truth -* Composable route atoms - build nested/dynamic routes out of small, independent pieces -* Framework-agnostic core (`jarl-atoms`) with lightweight React bindings (`jarl-react`) -* Full querystring matching support -* Resolve promises during routing (via jotai's own async atoms) and redirect if required -* SSR/SSG-safe: the resolved location atom is hydratable per-render on the server -* And much more... +- Map URLs directly to state (and back again) - the URL becomes the source of truth +- Composable route atoms - build nested/dynamic routes out of small, independent pieces +- Framework-agnostic core (`jarl-atoms`) with lightweight React bindings (`jarl-react`) +- Full querystring matching support +- Resolve promises during routing (via jotai's own async atoms) and redirect if required +- SSR/SSG-safe: the resolved location atom is hydratable per-render on the server +- And much more... ## Concrete Example @@ -54,9 +54,9 @@ Declare some route atoms: ```ts // routes.ts -import { rootAtom, staticRouteAtom, paramRouteAtom } from "jarl-atoms"; +import { rootRoute, staticRouteAtom, paramRouteAtom } from "jarl-atoms"; -export const homeRoute = rootAtom; +export const homeRoute = rootRoute; export const aboutRoute = staticRouteAtom("about"); export const productsRoute = staticRouteAtom("products"); // The `productId` segment is bound into `values` when this route matches: @@ -75,7 +75,7 @@ import App from "./App"; createRoot(document.getElementById("root")!).render( - + , ); ``` @@ -110,7 +110,9 @@ import { Link } from "jarl-react"; const MainMenu = () => (