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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@hazae41/next-as-immutable

v1.0.1

Published

Create immutable Next.js webapps

Downloads

29

Readme

Next.js as Immutable

Create immutable Next.js webapps that are secure and resilient.

npm i -D @hazae41/next-as-immutable

Node Package 📦

Examples

Here is a list of immutable Next.js webapps

  • https://dstorage.hazae41.me/v0 / https://github.com/hazae41/dstorage

Setup

Install @hazae41/immutable

npm i @hazae41/immutable

Install @hazae41/next-as-immutable as devDependencies

npm i -D @hazae41/next-as-immutable

Modify your package.json to add node ./scripts/build.mjs in order to postprocess each production build

"scripts": {
  "dev": "next dev",
  "build": "next build && node ./scripts/build.mjs",
  "start": "npx serve --config ../serve.json ./out",
  "lint": "next lint"
},

Modify your next.config.js to use exported build, immutable build ID, and immutable Cache-Control headers

const { withNextAsImmutable } = require("@hazae41/next-as-immutable")

module.exports = withNextAsImmutable({
  /**
   * Your Next.js config
   */
})

Create a ./serve.json file with this content

{
  "headers": [
    {
      "source": "**/*",
      "headers": [
        {
          "key": "Cache-Control",
          "value": "public, max-age=31536000, immutable"
        }
      ]
    }
  ]
}

Create a ./public/start.html file with this content

<!DOCTYPE html>
<html>

<head>
  <title>Loading...</title>
  <script type="module">
    try {
      const latestScriptUrl = new URL(`/service_worker.latest.js`, location.href)
      const latestScriptRes = await fetch(latestScriptUrl, { cache: "reload" })

      if (!latestScriptRes.ok)
        throw new Error(`Failed to fetch latest service-worker`)
      if (latestScriptRes.headers.get("cache-control") !== "public, max-age=31536000, immutable")
        throw new Error(`Wrong Cache-Control header for latest service-worker`)

      const { pathname } = latestScriptUrl

      const filename = pathname.split("/").at(-1)
      const basename = filename.split(".").at(0)

      const latestHashBytes = new Uint8Array(await crypto.subtle.digest("SHA-256", await latestScriptRes.arrayBuffer()))
      const latestHashRawHex = Array.from(latestHashBytes).map(b => b.toString(16).padStart(2, "0")).join("")
      const latestVersion = latestHashRawHex.slice(0, 6)

      const latestVersionScriptPath = `${basename}.${latestVersion}.js`
      const latestVersionScriptUrl = new URL(latestVersionScriptPath, latestScriptUrl)

      localStorage.setItem("service_worker.current.version", JSON.stringify(latestVersion))

      await navigator.serviceWorker.register(latestVersionScriptUrl, { updateViaCache: "all" })
      await navigator.serviceWorker.ready

      location.reload()
    } catch (e) {
      console.error(e)

      alert(`Failed to load the latest version of the webapp.`)

      return
    }
  </script>
</head>

</html>

Create a ./scripts/build.mjs file with this content

import crypto from "crypto"
import fs from "fs"
import path from "path"

export function* walkSync(dir) {
  const files = fs.readdirSync(dir, { withFileTypes: true })

  for (const file of files) {
    if (file.isDirectory()) {
      yield* walkSync(path.join(dir, file.name))
    } else {
      yield path.join(dir, file.name)
    }
  }
}

/**
 * Replace all .html files by start.html
 */

for (const pathname of walkSync(`./out`)) {
  if (pathname === `./out/start.html`)
    continue

  const dirname = path.dirname(pathname)
  const filename = path.basename(pathname)

  if (!filename.endsWith(".html"))
    continue

  fs.copyFileSync(pathname, `./${dirname}/_${filename}`)
  fs.copyFileSync(`./out/start.html`, pathname)
}

fs.rmSync(`./out/start.html`)

/**
 * Find files to cache and compute their hash
 */

const files = new Array()

for (const pathname of walkSync(`./out`)) {
  if (pathname === `./out/service_worker.latest.js`)
    continue

  const dirname = path.dirname(pathname)
  const filename = path.basename(pathname)

  if (fs.existsSync(`./${dirname}/_${filename}`))
    continue
  if (filename.endsWith(".html") && fs.existsSync(`./${dirname}/_${filename.slice(0, -5)}/index.html`))
    continue
  if (!filename.endsWith(".html") && fs.existsSync(`./${dirname}/_${filename}/index`))
    continue

  const text = fs.readFileSync(pathname)
  const hash = crypto.createHash("sha256").update(text).digest("hex")

  const relative = path.relative(`./out`, pathname)

  files.push([`/${relative}`, hash])
}

/**
 * Inject `files` into the service-worker and version it
 */

const original = fs.readFileSync(`./out/service_worker.latest.js`, "utf8")
const replaced = original.replaceAll("FILES", JSON.stringify(files))

const version = crypto.createHash("sha256").update(replaced).digest("hex").slice(0, 6)

fs.writeFileSync(`./out/service_worker.latest.js`, replaced, "utf8")
fs.writeFileSync(`./out/service_worker.${version}.js`, replaced, "utf8")

Add this glue code to your service-worker

import { Immutable } from "@hazae41/immutable"

declare const self: ServiceWorkerGlobalScope

self.addEventListener("install", (event) => {
  /**
   * Auto-activate as the update was already accepted
   */
  self.skipWaiting()
})

/**
 * Declare global template
 */
declare const FILES: [string, string][]

/**
 * Only cache on production
 */
if (process.env.NODE_ENV === "production") {
  const cache = new Immutable.Cache(new Map(FILES))

  self.addEventListener("activate", (event) => {
    /**
     * Uncache previous version
     */
    event.waitUntil(cache.uncache())

    /**
     * Precache current version
     */
    event.waitUntil(cache.precache())
  })

  /**
   * Respond with cache
   */
  self.addEventListener("fetch", (event) => cache.handle(event))
}

Use Immutable.register(pathOrUrl) to register your service-worker in your code

e.g. If you were doing this

await navigator.serviceWorker.register("/service_worker.js")

You now have to do this (always use .latest.js)

await Immutable.register("/service_worker.latest.js")

You can use the returned async function to update your app

navigator.serviceWorker.addEventListener("controllerchange", () => location.reload())

const update = await Immutable.register("/service_worker.latest.js")

if (update != null) {
  /**
   * Update available
   */
  button.onclick = async () => await update()
  return
}

await navigator.serviceWorker.ready

You now have an immutable but updatable Next.js app!