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

@evolving-edge/ee-cdn-cli

v0.3.1

Published

Publish a static site to the Evolving Edge CDN — a CLI for any generator, plus an Astro integration.

Readme

@evolving-edge/ee-cdn-cli

Publish a static site to the Evolving Edge CDN.

Your build is packaged into a single content-addressed .ee workload and sent to the control plane. Edge nodes resolve your domain to that content hash and serve it, sourcing bytes from peer nodes in-region before falling back to origin.

Two ways in: a CLI for any generator, and an Astro integration.

CLI

Works with anything that emits a directory — Hugo, Eleventy, Jekyll, Zola, Publii, hand-written HTML.

npx @evolving-edge/ee-cdn-cli ./public --domain blog.example.com --project proj_abc
ee-deploy [directory] --domain <host> [options]

  --project <id>           Project ID. Required if your token is project-scoped.
  --level <0|1|2>          Encryption level. Default 0.
  --trailing-slash         Redirect /about to /about/.
  --clean-urls             Serve /about from about.html.
  --builder-version <v>    Pin ee-builder instead of using "latest".
  --dry-run                Package but do not upload.
  --help                   Everything else.

The directory is optional. Without it, ee-deploy finds the build output from the project itself (package.json dependencies or the generator's config file): Astro, Next.js static export, Nuxt, SvelteKit, Docusaurus, VitePress, Gatsby, Hexo, Eleventy, Hugo, Jekyll, MkDocs, mdBook, Sphinx, Zola and Vite. It prints what it chose (Deploying ./dist (detected: Astro)), and it never guesses: with no match, two matches, or no index.html in the folder, it stops and asks for the directory.

Routing flags. Most generators link to /about while writing about/index.html; --trailing-slash makes the edge redirect /about to /about/. Generators that write about.html instead (Quartz, Observable Framework, vite-ssg, VitePress with cleanUrls) want --clean-urls. Each flag writes its line into the output's _redirects (creating the file if needed), so the setting travels with the bundle. A site whose own _redirects already sets the pragma keeps its value, off included.

Pin ee-builder in CI. By default the CLI downloads the latest ee-builder and records its checksum in .ee-builder-lock.json. With "latest" plus a committed lockfile, every new builder release fails the deploy with a checksum mismatch, by design. Pin a version instead (--builder-version or EE_BUILDER_VERSION, using the label from the "Upload ee-builder to CDN" run summary), and commit the lockfile.

The deploy token comes from the portal (Workload → Deploy Tokens) and is read from EE_CDN_TOKEN; EE_CDN_PROJECT_ID and EE_CDN_BUILD_ID supply defaults for --project and --build.

Publii: set deployment to Manual, which writes the site to a folder, then point ee-deploy at that folder.

Astro

npm install -D @evolving-edge/ee-cdn-cli
// astro.config.mjs
import { defineConfig } from 'astro/config';
import eeCdn from '@evolving-edge/ee-cdn-cli/astro';

export default defineConfig({
  site: 'https://blog.example.com',
  trailingSlash: 'always',        // required — see below
  build: { format: 'directory' }, // required — see below
  integrations: [
    eeCdn({
      deploy: { domain: 'blog.example.com', projectId: 'proj_…' },
    }),
  ],
});

Set EE_CDN_TOKEN to a deploy token from the portal (Workload → Deploy Tokens). Nothing else is required — the ee-builder binary is downloaded and cached automatically.

Put it last in integrations

Astro runs astro:build:done hooks in integration order, and this one packages the whole of dist/. Any integration that writes to dist/ in its own astro:build:done — @astrojs/sitemap is the common one — must run first, or its output is not in the workload you publish.

This fails silently: dist/ on disk looks correct afterwards, because the sitemap is written a moment later. The only visible symptom is that the deployed site is missing files. To check, compare the hash the integration logs against an independent one:

ee-builder -src dist -hash-only    # must equal the logged "Content hash"

Two required Astro settings

trailingSlash: 'always' and build: { format: 'directory' } are not stylistic preferences.

Edge nodes resolve request paths literally: index.html is appended only when the path already ends in /, and there is no .html extension fallback. So build.format: 'file' emits page.html while linking to /page, and every page on the site 404s.

A request for /page without the slash 404s too, unless you opt in to the trailing-slash redirect: add the line # ee:trailing-slash on to public/_redirects, and the edge answers /page with a 301 to /page/. That rescues links from elsewhere; trailingSlash: 'always' keeps your own links from taking the extra hop. Redirects work on Level 0 sites only.

The integration throws on format: 'file' at config time rather than letting you find out in production, and warns on trailingSlash: 'never'.

Options

| Option | Type | Default | Notes | |---|---|---|---| | deploy | object | — | Omit to build and validate without publishing. | | deploy.domain | string | — | Hostname edge nodes serve this site on. | | deploy.level | 0 \| 1 \| 2 | 0 | 0 = compressed only. Use 0 for public sites. | | deploy.projectId | string | — | Required if your deploy token is project-scoped. | | deploy.buildId | string | — | Applies the Build Config's server-side defaults. | | deploy.trailingSlash | boolean | true | Writes # ee:trailing-slash on into dist/_redirects, so a bare /about redirects to /about/ instead of 404ing. Set false to leave _redirects alone. | | controlPlane | string | https://cp.3dge.app | | | token | string | $EE_CDN_TOKEN | | | builderPath | string | — | Use a local ee-builder instead of downloading. | | builderVersion | string | latest | Pin a published ee-builder version; see "Pin ee-builder in CI". | | builderChecksum | string | — | Pin the binary's sha256. | | workloads | array | [] | Encrypted sub-bundles. Not needed for a normal site. | | workloads[].embedKey | boolean | false | Level 1 only. Bake the key into the built page instead of delivering it out of band. See below. | | sdkUrl | string | false | ${cdnUrl}/ee.js | Injected only when data-ee elements exist. | | upload | boolean | true | | | dryRun | boolean | false | Build and hash, stop before uploading. | | copyTo | string | — | Also copy dist here. Overwrites in place. |

Encrypted workloads

For content that should not be served in the clear, build it as a separate workload and reference it from HTML:

eeCdn({
  workloads: [{ name: 'secure', src: 'secure-content', level: 1 }],
  deploy: { domain: 'app.example.com' },
})
<img data-ee="/images/photo1.svg" data-ee-workload="secure" />

At build time each data-ee element has its workload name rewritten to a content hash, and the browser SDK is injected into <body>. Keys are stable across deploys as long as the source bytes do not change — that is what .ee-cache.json is for; commit it.

A Level 0 site has no data-ee elements, so no script is injected and no JavaScript ships. That is deliberate.

Level 1 key delivery

A Level 1 key decrypts client-side and belongs to whoever holds the page — it is never registered with the control plane, so nothing there can revoke or audit access to it the way Level 2's short-lived tokens do. Because of that, the build does not write the key into the HTML it produces by default: every edge node and every cache in front of your site would then be serving the key in the clear to anyone who requests the page, which defeats the point of keeping it out of server hands in the first place.

Instead, the build logs each Level 1 workload's key once (.ee-cache.json also keeps a copy locally, keyed by workload) and expects you to deliver it out of band — typically as a URL fragment, which a conforming browser never sends to any server:

<script src="https://cdn.3dge.app/ee.js" data-workload="<hash>"></script>
https://app.example.com/gated-page#key=<the-logged-key>

Whoever hands out that link — an email, a purchase confirmation, your own authenticated redirect — is what gates access; the CDN and edge nodes never see the key. This is the same shape the portal uses for Level 1 (#208).

If you'd rather have a link that works without a fragment and accept that the key then reaches every edge node and cache in the clear, set workloads[].embedKey: true on that workload and data-ee-key is written into the page as before.

The #key= fragment carries one workload's key at a time. If a single page references more than one distinct Level 1 workload, only the one matching fragment resolves automatically — give the others embedKey: true, or deliver each key through your own script before the SDK's auto-init runs.

ee-builder integrity

The control plane publishes a .sha256 sidecar for edge-node but not for ee-builder. Until it does, the first download's digest is recorded in .ee-builder-lock.json and every later download must match it. Commit that lockfile so CI verifies the same binary you built against. Pass builderChecksum to pin a digest explicitly instead.

Published targets are linux/amd64, linux/arm64, darwin/arm64, darwin/amd64 and windows/amd64. On anything else, build from source and pass builderPath.

Custom domains

ee-domain claims a hostname for a project and prints the two DNS records to add at your DNS provider. With --wait, it checks until the claim is active and the certificate is issued:

ee-domain claim blog.example.com --project proj_abc --wait
ee-domain status blog.example.com
ee-domain verify blog.example.com   # check the TXT record now

It reads EE_CDN_TOKEN and EE_CDN_PROJECT_ID like ee-deploy. Use a project-scoped deploy token, which manages its own project's domains, or an admin token. Global deploy tokens are refused.

After a deploy

Propagation takes up to about 90 seconds: the control-plane alias cache is 60s, the edge domain cache 30s, and the node heartbeat 30s.

Deploys are content-addressed and immutable, so rolling back is repointing an alias — the previous bundle is still there.

Known CDN limitations

These are properties of the CDN, not of this package:

  • Default cache headers. HTML revalidates on every request (max-age=0, must-revalidate, with an ETag), files under _astro/ (and a few other known build folders) are cached for a year as immutable, and everything else for an hour with stale-while-revalidate. Level 1 and 2 sites are served no-cache. On Level 0 a _headers file can set your own Cache-Control, for example a year for your generator's hashed-asset folder; see Response headers for per-generator rules.
  • One site-wide 404. A miss serves your root 404.html with a 404 status, or the platform's own 404 page if you have none. There are no per-directory 404 pages.
  • The trailing-slash redirect is opt-in. /page 404s where /page/ works, unless _redirects contains # ee:trailing-slash on.
  • Clean URLs are opt-in. A generator that writes page.html but links to /page (Quartz, Observable Framework, vite-ssg, VitePress with cleanUrls, and others) needs # ee:clean-urls on in _redirects, which serves /page from page.html without a redirect. Astro doesn't need it: it writes page/index.html.
  • Redirects are Level 0 only. A Netlify-format _redirects file supports 301, 302, 200 (rewrite) and 404 rules. A Netlify-format _headers file sets response headers on Level 0 and Level 2 sites, not Level 1.
  • Static only. No SSR, no adapters.
  • A custom domain needs a claim. Its certificate is issued once the claim's DNS records are verified; see Custom domains.

Releasing

Releases are cut with Changesets. The version number is reviewed as a diff before it exists, rather than typed into package.json by whoever remembered.

In a PR that changes behaviour, add a changeset:

npm run changeset

It asks whether the change is major, minor or patch and for a sentence describing it. Write that sentence for somebody installing the package, not for somebody reading the diff — it becomes the changelog entry. Docs-only or CI-only PRs do not need one.

Merging that PR opens (or updates) a Release: version packages PR, which applies every pending changeset: bumps package.json and writes CHANGELOG.md.

Merging the version PR is the release. CI sees a version the registry does not have and ships it.

Publishing, and why a green run may not mean shipped

Publishing uses npm trusted publishing (OIDC) — there is no NPM_TOKEN anywhere.

What the final step does depends on one setting on npmjs.com. Allowed actions on a trusted publisher default to staging only; direct npm publish is a separate, unticked box. So:

  • If npm publish is ticked, the version goes straight to the registry and the release is complete when CI is green.

  • If it is not, CI stages the version instead. It is uploaded but not installable until a maintainer approves it with 2FA:

    npm stage list @evolving-edge/ee-cdn-cli
    npm stage approve <stage-id>

    or at npmjs.com → Staged Packages.

The workflow tries to publish and falls back to staging only on a 403 that names the action, so ticking the box is the only thing needed to go fully automatic — no code change. The run summary always says which of the two happened.

Staging is npm's default for a reason: it puts a person between a compromised workflow and the registry. Ticking the box trades that for not having to approve each release.

License

MIT