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

@codefusion-cc/app-update

v0.1.2

Published

Keeps people on the newest version: moves an open page onto a newer deploy without losing anyone's work, and tells an installed app's users about a newer GitHub release, what it brings and whether theirs is behind; in an app on an iPhone's Home Screen, op

Readme

@codefusion-cc/app-update

Keeps people on the newest version. A page: an installed web app or a tab left in the background can run old code for days while the Worker behind it is new, and after a deploy a page may ask for scripts that no longer exist; this package notices both and moves the page onto the new build at a moment nothing would be interrupted. An installed app released on GitHub: which release is newest, whether the version someone runs is behind, what each release brings, and release notes people can read, written from the merged pull requests.

npm install @codefusion-cc/app-update

The deploy publishes /version.json with its commit: versionFile() from @codefusion-cc/console/vite writes it, and the page knows its own commit from buildIdentity() (e.g. as a Vite define).

In the page

Once, at startup, in a module the rest of the app imports:

// src/updates.ts
import { watchAppUpdates } from '@codefusion-cc/app-update/browser'

export const updates = watchAppUpdates({ current: __APP_COMMIT__, storageKey: 'myapp-update' })

Imported outside a browser (a Node test, a server render) it watches nothing, and importOrReload only imports.

The page checks the version file when it opens, every 10 minutes while in view (checkEveryMs), and whenever the person comes back to it. Knowing of a newer build:

  • a link the person clicks loads its page afresh instead of a router move;
  • coming back to the page (another app, a locked phone) reloads it at once;
  • with reloadWhenIdle: true, the page reloads as soon as nothing holds it, trying again every 30 seconds, in view or in the background (where nobody sees it; the version is then checked in the background too): for pages people leave open for hours, like a chat.

Nothing is cut short. The reload waits while:

  • a field was changed on this page (until the router moves on: call updates.navigated() on each route change), or a focused field holds text;
  • a request that changes data (anything but GET and HEAD) or a Web Lock task runs (holdDuringWrites, on by default: the package wraps fetch and navigator.locks.request);
  • the app holds it: const release = updates.hold() for an unsaved form or a call, or updates.whileHeld(task) for work that must finish (the browser also asks before the page is closed).

When the page cannot reload by itself, updates.onNewVersion(commit => …) tells the app, once per version, so it can offer a refresh. A page reloads into each version at most once per tab (the tab's sessionStorage remembers), so a cache serving the old build cannot make it loop; without that storage it never reloads by itself.

Scripts a deploy removed

A lazily loaded page whose files are gone reloads into the new build and keeps its loading state:

const Settings = lazy(() => updates.importOrReload(() => import('./Settings.tsx')).then((m) => ({ default: m.Settings })))

Vite's vite:preloadError and a failed <link rel="modulepreload"> reload too, and the package takes Vite's error while the page reloads for it, so no error screen or failure report shows meanwhile. Such a reload happens at most once a minute, so a file missing for good shows its error instead of looping. It waits for the same holds as a reload into a newer build (a changed field, someone typing, a write, a Web Lock task, hold, whileHeld) and comes as soon as the last one lets go (someone typing: on the next try, every 30 seconds), a hidden page too; meanwhile Vite's error goes on and onNewVersion hears of the newer build, so the app can show its error and offer the refresh. Failures count as expected for the first minute of that wait only, so a file missing for good on a page held for long is still reported. A page still here 10 seconds after setting out to reload (RELOAD_WAIT_MS: still held, or the person chose to stay when the browser asked) gets the import's error.

Offline (navigator.onLine false) the page never reloads or sends a link to a fresh load: that would land on the browser's offline page. A script that fails then is the network's doing, so the import's error goes to the app (its error screen and retry), and a newer build stays a notice until the connection is back and the deploy answers, when a waiting reload comes. Coming back to a page reloads it into a newer build only when its version check just got an answer, so a device that says it is online but reaches nothing keeps the notice too.

A reload waiting for a hold comes on the task after the hold lets go, not inside it: writes made one after another (await save(); await publish()) hold it from the first to the last. updates.isReloading() says a reload is on its way (failures now are expected: don't report them), and updates.reloadForStaleScript() is the same reload for an error the app caught itself.

Releases of an installed app

@codefusion-cc/app-update (the root entry) runs wherever fetch does: a Worker, a page, Node.

import { fetchGitHubReleases, isOutdated, latestRelease, releasesSince } from '@codefusion-cc/app-update'

const result = await fetchGitHubReleases('owner/app', {
  userAgent: 'app.example.com',
  init: { cf: { cacheTtl: 600, cacheEverything: true } }, // in a Worker: GitHub asked at most every 10 minutes
})
if (result.ok) {
  const latest = latestRelease(result.releases) // the highest version that is not a pre-release
  if (isOutdated(device.version, latest?.version)) showUpdate(latest, releasesSince(result.releases, device.version))
} else {
  // 'offline' | 'rate-limited' (with retryAt when GitHub says) | 'not-found' | 'unavailable' | 'invalid'
}

Versions follow Semantic Versioning 2.0: 1.2.0-rc.1 comes before 1.2.0, 1.2.0-beta.11 after 1.2.0-beta.2, build metadata never counts (1.2.0+dev is 1.2.0, a good spelling for development builds), and a leading v is fine. compareVersions is null and isOutdated false when either side is not a version, so a broken report never nags anyone. fetchGitHubReleases never rejects: no network, a timeout, the rate limit and odd answers are reasons; drafts and tags that are not versions are left out.

Release notes on screen

parseReleaseNotes(release.notes, { repo }) reads the Markdown release notes use (headings, lists, paragraphs, code, emphasis, links, #123, @name) into blocks of plain data the app renders with its own components. Nothing is HTML: markup stays visible text and links only ever go to http(s) or mailto. A bare pull request address reads #12, a comparison v1.0.0...v1.1.0; #123 and @name link only with repo, and web names another GitHub's pages.

In React

@codefusion-cc/app-update/react has what a page shows of all this, styled by the app:

import { BuildVersion, createLatestRelease, ReleaseNotes } from '@codefusion-cc/app-update/react'

// One read for every component on the page, again after 10 minutes (30 s after a failure).
export const latest = createLatestRelease(() => fetch('/api/releases/latest').then(r => (r.ok ? r.json() : null)))

function WhatsNew({ release }) {
  const newest = latest.useLatest() // undefined while read, null when unknown
  return (
    <>
      <ReleaseNotes notes={release.notes} repo="owner/app" classes={{ list: 'list-disc pl-5', link: 'link' }} />
      <BuildVersion version="1.2.0" commit="abc1234" repo="owner/app" />
    </>
  )
}

ReleaseNotes takes Markdown or blocks from parseReleaseNotes and renders text and safe links only (new tab by default); its headings start at h3, under the release's own title. BuildVersion shows v1.2.0 · abc1234 with the commit linked (commitUrl, also in the root entry), and a build outside git (dev) as it is.

Release notes people can read

codefusion-release-notes writes a release's notes from the pull requests it merged, grouped by the branch type (feat/ New, fix/ Fixes, perf/ Faster, docs/ Documentation, the rest Under the hood, Renovate's on one line), else a Conventional Commits title, else a common label (bug, enhancement, dependencies…). The release's own pull request and those labeled skip-changelog stay out. writeReleaseNotes takes messages to write them in another language (ENGLISH is the default).

# .github/workflows/release.yml, on a pushed v* tag
- run: npx -y -p @codefusion-cc/app-update@^0.1 codefusion-release-notes "$GITHUB_REF_NAME" > notes.md
  env: { GITHUB_TOKEN: '${{ github.token }}' }
- uses: softprops/action-gh-release@v2
  with: { body_path: notes.md }

The release before is the highest version tag below this one (pre-releases count only for a pre-release); --previous names another. @codefusion-cc/app-update/release-notes has the same as functions (writeReleaseNotes, mergedPulls, previousTag) for a script of its own.

Links to other sites from the Home Screen

An app added to an iPhone's or iPad's Home Screen opens links to other sites in a browser sheet that shares none of Safari's sign-ins, so every link to npm, GitHub or a dashboard asks the person to sign in again. Once, at startup:

import { openLinksInSafari } from '@codefusion-cc/app-update/browser'

openLinksInSafari()

There, a click on an https link to another origin goes to x-safari-https://…, which iOS 17 opens in Safari; when the app is still on screen 600 ms later (SAFARI_FALLBACK_MS: an older iOS), the link opens as it would have. Links the app handles itself (preventDefault), downloads and clicks with a modifier are left alone. In a browser tab, on desktop and on Android nothing changes. It returns a function that stops it.

In an app's tests

@codefusion-cc/app-update/testing is a page and the deploy behind it, for tests of the app's code around the package (a form that holds the reload, a task under whileHeld):

import { testPage } from '@codefusion-cc/app-update/testing'

const page = testPage({ deployed: 'e461357' })
const updates = watchAppUpdates({ current: 'abc1234', storageKey: 'test' }, page.env)
const release = updates.hold()
await page.comeBack()
expect(page.reloads).toBe(0)
release()
await page.comeBack()
expect(page.reloads).toBe(1)

Tests

Written from how a page meets a deploy: links of every kind, coming back, the back-forward cache, blocked storage, writes and Web Locks that fail, double events, a cache serving the old build, and scripts missing for good. Releases: the ordering Semantic Versioning lists, with property tests of a total order; GitHub offline, slow, rate limited both ways, missing, refusing and answering nonsense; hostile notes (script links, HTML, unclosed markers, 128 KiB of one marker) and notes in any script; pull requests merged twice, squashed, never merged, and comparisons over several pages.