npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tsquid/routes

v0.3.0

Published

Typed routes for React Router and Relay entrypoints: URI builders, route context, and preloading links.

Readme

@tsquid/routes

Runtime for routes generated by @tsquid/codegen: typed URIs, route context, Relay entrypoint loading, preloading links.

Peers: react 19, react-router 8, react-relay / relay-runtime 21.

Config

routes.json declares the URL shape. The entrypoint module declares behaviour (getRouteType, getPreloadProps).

{
  "runtime": "@tsquid/routes",
  "routes": [{
    "name": "ArticleSearch",
    "entryPoint": "src/components/article/Article.entrypoint.ts",
    "path": "/articles/:section",
    "fields": {
      "section": { "kind": "string" },
      "page": { "kind": "int", "optional": true },
      "tags": { "kind": "string[]", "optional": true },
      "drafts": { "kind": "bool", "optional": true }
    }
  }]
}
tsquid-codegen routes.json src/routes/__generated__/routes.ts [--check]
  • Kinds: string, int (safe integer), bool, and [] arrays of each.
  • Path fields are required scalars. Every other field is a query parameter.
  • Arrays are repeated keys: order and duplicates are preserved. An empty optional array is omitted; a required array must be non-empty.
  • Invalid values, duplicate scalar keys, missing required fields and bad path encoding throw. Nothing is coerced.
  • An optional path segment is modelled as a separate route.
  • entryPoint is a normalized path relative to the config, named <Name>.entrypoint.ts or <Name>.entrypoint.tsx. Basenames must be unique, including across .ts and .tsx.
  • ssr (default true): whether servers may inline initial data for the route.
  • query (optional): the Relay operation a server runs for the route. See Initial data.
  • persistedQueries: Relay's persisted-queries file, relative to the config. Required when any route has a query.

Generated API

For each route: <Route>InputType, <Route>URI and <Route>RouteContext. For each entrypoint module: use<Name>RouteContext(). Across the catalogue: the RouteName enum.

ArticleSearchURI.getURI({ section: 'a/b', tags: ['x', 'y'] }); // '/articles/a%2Fb?tags=x&tags=y'
ArticleSearchURI.parseURI('/articles/news?drafts=false');      // { section: 'news', drafts: false }

const route = useArticleRouteContext();                        // union of the module's routes
if (route.currentRoute === RouteName.ArticleSearch) {
  navigate(route.updateURI({ page: 2 }));                      // narrowed to ArticleSearch fields
}
  • getURI builds a URL from explicit input and works from anywhere.
  • updateURI(patch) exists only on the active route context. It merges the patch into the current location, removes keys set to undefined, and keeps query keys the route doesn't own, plus the hash. It returns a string; it doesn't navigate.
  • use<Name>RouteContext() throws outside a provider, or when another entrypoint is active. <Route>RouteContext.useRoute() requires that exact route.
  • One context per catalogue, so the nearest route provider wins.

Entrypoints

export function ArticleEntryPoint({ queries }: EntryPointPropsOf<typeof ARTICLE_ENTRY_POINT>) {
  return createElement(ArticlePage, { queries });
}

export const ARTICLE_ENTRY_POINT: RouteEntryPoint<ArticleEntryPointParams, Queries> = {
  root: resource('ArticlePage', ArticleEntryPoint),
  getPreloadProps: route => {
    switch (route.currentRoute) {
      case RouteName.ArticleSearch:
        return { queries: { search: { parameters: ArticleSearchQuery, variables: { section: route.input.section } } } };
      case RouteName.ArticleDetail:
        return { queries: { detail: { parameters: ArticleDetailQuery, variables: { id: route.input.id } } } };
    }
  },
};

export const ARTICLE_ROUTES = {
  search: defineRoute({ uri: ArticleSearchURI, context: ArticleSearchRouteContext, entryPoint: ARTICLE_ENTRY_POINT }),
};
  • Unused slots default to the exported Empty (Record<string, never>): EntryPointProps<Q, N = Empty, P = Empty, E = Empty> and RouteEntryPoint<R, Q, N = Empty, P = Empty, E = Empty>.
  • EntryPointPropsOf<typeof entrypoint> extracts the root component's props.

For derived route types, provide getRouteType and type the entrypoint with that result instead:

getRouteType: (input, currentRoute) => ({ currentRoute, ...input, tags: new Set(input.tags ?? []) })
  • RouteEntryPoint types Relay's root + getPreloadProps contract. Relay's own EntryPoint types are circular and fail under TS7; runtime behaviour is Relay's.
  • getPreloadProps returns queries, entryPoints (nested: entryPoint + entryPointParams) and optional extraProps. Make keys optional in their type to omit them conditionally.
  • EntryPointProps<Queries, Nested, RuntimeProps, ExtraProps> types the root component's props. Render nested references with RouteEntryPointContainer.
  • resource(id, Component) wraps an eager component. lazyResource(id, () => import('./Page').then(m => m.Page)) shares one load between preload and navigation, exposes the loaded component synchronously, and retries after failure. Speculative query/module failures release their preload retains and subscriptions.
  • Without getRouteType, params are { currentRoute, input }. The generated <Entry>EntryPointParams union narrows input by currentRoute in getPreloadProps.
  • getRouteType(input, currentRoute) optionally maps URL input to another type (sets, enums, dates).
  • Components read route state with use<Entry>RouteContext(); avoid copying it into entrypoint extraProps.
  • useRouteRefresh() returns the active route's refresh callback. After mutations, call it to reload every query, including nested entrypoints, with network-only. React transitions keep previous content visible while loading; Relay supplies fresh query fetch keys and disposes replaced references. Normal preloads retain their configured/default fetch policy.
  • defineRoute(...).Root is the router component; mount it under RelayEnvironmentProvider. It accepts fallback. It loads with useEntryPointLoader in a layout effect (disposed on replace and unmount), so data already in the store or answered synchronously renders without the fallback. Pass errorFallback: ({ error, retry }) => ReactNode to render route loading/rendering errors with a network-only retry. Errors reset on navigation; without a fallback they reach the router's error boundary. useRouteType() reads the derived route type.

Mutations

useMutationAction<Mutation>(artifact) from @tsquid/routes/mutation returns [commit, isPending]. await commit({ variables, updater?, ... }) resolves with the artifact's response type or rejects with network/GraphQL errors. Completion callbacks are replaced by the promise; after unmount promises remain unsettled. Store invalidation remains app code; use useRouteRefresh() after saving when the route needs fresh server data.

Links

const AppLink = createLink([ARTICLE_ROUTES.search, ...]);
  • A React Router Link that preloads the matching route's entrypoint graph on hover and focus. Routes are matched in order, so put static paths before dynamic ones that conflict.
  • Accepts to or anchor href props and forwards ref, so it can be used as Astryx's LinkProvider component. to keeps router navigation; same-origin matched href links route and preload. Unmatched href, external, download, non-_self and reloadDocument links use <a>.
  • href is router-relative under a basename: /profile becomes /app/profile when the router basename is /app, matching generated getURI() paths.
  • Preloading uses fetchQuery plus retain, per Relay's retention contract. It is released on failure, destination or environment change, unmount, or after 30 s. Failed speculative loads allow the next hover to retry. Duplicate requests are deduplicated by Relay.
  • Skipped for external links, downloads, reloadDocument, non-_self targets and preventDefault. Preload errors are swallowed; navigation retries.
  • Requires concrete query artifacts (not preloadable IDs) and a single Relay environment. A cyclic entrypoint graph throws and releases what it acquired.

Network

createFetchGraphQL({ url, fetch?, headers?, credentials? }) from @tsquid/routes/network posts { query, operationName, variables } and returns a Relay observable. Persisted-only operations send { id, operationName, variables }. Unsubscribing aborts the request; credentials default to same-origin. headers(request) may be asynchronous (for mutation CSRF tokens). HTTP failures and responses without data throw; partial data plus errors passes through to Relay.

For sign-out cancellation, supply a fetch wrapper that combines init.signal with an app-owned signal via AbortSignal.any; abort the latter on sign-out.

withInitialData(createFetchGraphQL({ url: "/graphql" }), readInitialData()) composes network fetching with server-inlined responses.

Initial data

tsquid renders on the client. A server, in any language, can run a route's query and inline the response, so the route renders without waiting on it. The server side is specified in the tsquid-server-integration skill, with conformance vectors.

{ "persistedQueries": "persisted_queries.json",
  "routes": [{ "name": "Posts", "path": "/posts", "query": "PostsQuery", "fields": {
    "order_by": { "kind": "string", "optional": true }, "preview_id": { "kind": "int", "optional": true } } }] }
  • A route query's variables come from the URL only: each binds the field with the same name, or falls back to its GraphQL default, or null. The generator rejects queries that need anything else: required variables without a field, incompatible kinds, @defer/@stream, provided variables, non-queries.
  • The entrypoint must preload that query with those variables: variables: PostsRouteQuery.getVariables(route.input). The generated helper includes every declared variable, defaults and nulls; operation names the query.
  • URL string fields bound to GraphQL enums remain strings. Narrow them in getRouteType before calling getVariables to satisfy Relay's enum variable type; the generic helper preserves that narrower input type. The generator has no schema enum catalogue and cannot validate or narrow enum values itself.
  • Everything else the route loads (other queries, nested entrypoints, conditional logic) is fetched by the client and suspends where it's read. Read it below its own <Suspense> so the rest renders first. Keep the default fetchPolicy (store-or-network): store-and-network refetches inlined data.
  • tsquid-codegen writes <output>.manifest.json: every route's path, fields, ssr flag and resolved query (operation, id, text, variables with bindings and defaults).

Relay must persist operations to the file the config names, keeping query text:

"persistConfig": { "file": "./persisted_queries.json", "include_query_text": true }

Relay only appends to that file, and skips unchanged operations. Regenerate it from empty, tsquid-relay persist, before tsquid-codegen. The generator rejects an operation name with more than one version. tsquid-relay check verifies persisted ids against compiled artifacts.

The server inlines { version: 1, responses: [{ operation, variables, response }] } as <script type="application/json" id="tsquid-data">.

import { readInitialData, withInitialData } from '@tsquid/routes/initialData';

new Environment({
  network: Network.create(withInitialData(fetchGraphQL, readInitialData(), { warn: import.meta.env.DEV })),
  store: new Store(new RecordSource()),
});
  • readInitialData(id = 'tsquid-data') returns null when the element is absent, and throws when it is malformed.
  • withInitialData answers each request matching an entry (operation name + variables) once, synchronously. Anything else goes to fetchFn, as do requests after the first request's task, when unused entries expire. Duplicate entries throw.
  • Variables match as Relay sends them: declared only, defaults included, top-level nulls ignored, object key order ignored.
  • warn logs boot misses and expired entries, with the exact operation and variables, so drift between the server and the entrypoint shows.