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

@sidebase/base-config

v0.1.0

Published

Shared @sidebase base configuration for Nuxt repos: ESLint / Prisma / tsconfig factories on npm, plus the streamctl file-sync preset payload

Readme

@sidebase/base-config

Shared @sidebase Nuxt configuration, delivered over two channels in one package:

  • npm channel: the root export @sidebase/base-config plus the subpath exports @sidebase/base-config/config, @sidebase/base-config/eslint, @sidebase/base-config/prisma, and @sidebase/base-config/tsconfig.base.
  • file-sync channel: the bundled presets/ payload read by the @sidebase/streamctl CLI.

Install

For the npm channel, install the package plus what the subpaths you use require:

pnpm add -D @sidebase/base-config eslint jiti

eslint (^10.5.0) and jiti (>= 2.2.0) are what /eslint needs; jiti is only required because the config file is TypeScript. prisma and @prisma/client (^6.19) are optional peers, install them only if you use /prisma.

jiti is not a declared peer -- peerDependencies is eslint, prisma and @prisma/client. It is in the command above because /eslint genuinely needs it, but since nothing declares it, pnpm emits no missing-peer warning if you leave it out. This README is the only thing that will tell you. See Requirements below.

For the file-sync channel, see Adopting below. The CLI wires the dependency itself.

Adopting

The payload is the install: @sidebase/streamctl reads presets/ straight from this package in a consuming repo's node_modules.

pnpm streamctl init --package @sidebase/base-config   # scaffold + first sync
pnpm streamctl check                                  # CI drift gate
pnpm streamctl upgrade --to <version>                 # move the pin forward

Neither package needs a registry token or private-registry setup: both target the public @sidebase scope on npm.

Coming from @sidestream-tech/nuxt-config? That rename is a manual migration, not an upgrade: streamctl upgrade moves the version pin and never rewrites package:. Follow docs/migration.md, and note that the step order is load-bearing.

Presets and profiles

Two separate axes in streamctl.config.ts:

  • base is the preset, the set of managed files you get. nuxt-app names the repo type: it ships a Dockerfile, a postgres/prisma CI pipeline, and PR-preview cleanup.
  • profile is the version baseline, the dependency pins reconciled into your package.json. It names the framework and its major: nuxt-4.

The major is in the profile name on purpose. Each profile carries its own detect.majorIs, so init picks the right one from your package.json and a future nuxt-5 profile can sit alongside nuxt-4 while repos migrate one at a time.

nuxt-4 is the only profile shipped today, so this payload is Nuxt 4 only. There is no nuxt-3 profile and none is planned. Running init in a Nuxt 3 repo fails with "No version profile could be detected", because no profile's detect.majorIs matches the nuxt version in your package.json. Move the repo to Nuxt 4 before adopting.

A profile name must match a versionProfiles key in the preset chain. If it does not, the CLI fails with CONFIG_INVALID: profile "..." is not declared in presets/manifest.json profiles[]. pnpm validate:presets catches the same mismatch at build time, so a broken profile name cannot ship in the first place.

Reconciled scripts.lint

The nuxt-4 profile reconciles your package.json scripts.lint to oxlint -c .oxlintrc.json --deny-warnings && eslint --max-warnings 0 ., so the fleet runs oxlint (already pinned, with the shipped .oxlintrc.json) before ESLint.

Unlike a version pin, a script has no forward-version floor to protect it, because a script is not a semver, so this is a plain overwrite: every sync replaces whatever your scripts.lint currently is. A repo that wants its own (for example ... && LINT_TYPEAWARE=true eslint --max-warnings 0 . to opt into type-aware linting) keeps it by excluding the key:

export default {
  // ...
  versionSyncExclude: ["scripts.lint"],
};

Overriding a managed file

.editorconfig is block-managed: the payload owns a marked region, delimited by # BEGIN streamctl MANAGED BLOCK editorconfig / # END ..., and preserves everything outside it byte-for-byte. On first sync the managed block is appended to the end of your file. Because EditorConfig applies later matching sections over earlier ones, you keep a project override by placing it below the managed block:

# BEGIN streamctl MANAGED BLOCK editorconfig
[*]
indent_size = 2
# END streamctl MANAGED BLOCK editorconfig

# your override wins because it comes after the managed block
[*]
indent_size = 4

That way the payload keeps updating its region on every sync while your indent_size = 4 survives. (A section placed above the block would be overridden by the block's indent_size = 2, so keep overrides below the END marker.)

First-sync hazard. If your repo already has an .editorconfig, the block writer appends without looking at what is there, so all of your existing content ends up above the block and therefore loses. streamctl check exits 0 either way, because the block strategy only inspects its own markers. This applies to a fresh streamctl init just as much as to a migration, and you have to fix it by hand once. Step 8 of docs/migration.md has the detector and the two-case fix.

If you instead want to stop managing a file entirely, opt it out per-path:

files: { ".editorconfig": "off" },

The cost of off is total: streamctl no longer touches that file, so every future payload fix to it stops reaching your repo, including new rules, security bumps, and format corrections. Prefer the block override above when you only need to change part of a file.

This repo's CLI dependency

The payload is validated and dogfooded against the CLI, so @sidebase/streamctl is a devDependency here, pinned to the published semver range. validate:presets imports the CLI's exported manifest schema and the e2e dry-run drives the real CLI binary, so both resolve @sidebase/streamctl from the registry.

The root streamctl.config.ts this payload documents needs streamctl >= 0.2.0, so the pin and the pnpm-workspace.yaml c12 override both move when that release lands. See "Config location" below.

Typed config

The root export @sidebase/base-config and the subpath @sidebase/base-config/config are the same module. Both provide defineNuxtBaseConfig and the NuxtBaseConfig type, which is what makes a streamctl.config.ts typed. Every consuming repo has this file, and streamctl init scaffolds it importing from the root export:

// streamctl.config.ts
import { defineNuxtBaseConfig } from "@sidebase/base-config";

export default defineNuxtBaseConfig({
  package: "@sidebase/base-config",
  base: "nuxt-app",
  version: "0.1.0",
  profile: "nuxt-4",
  ci: { unitTests: true },
});

Config location

streamctl >= 0.2.0 reads streamctl.config.ts from the repo root, and init scaffolds it there. The older .streamctl/config.ts is still resolved, so an existing repo keeps working untouched and can move the file whenever it likes.

Move it and nothing else changes: the contents are identical, and the CLI warns rather than guesses if both exist, naming the root file as the one it read. Keeping the legacy path is fine too. The one thing not to do is leave both in place, since only the root file is read and edits to the other will look like they do nothing.

If you set ignoresTypeAware yourself, list whichever path your repo uses. The default covers both.

defineNuxtBaseConfig returns the config unchanged. Its job is typed editor inference for this payload's knobs: completion and type checking on ci, versions, docker, automation, security, pnpm, and the whole eslint option surface, plus profile narrowed to the profile names that actually exist. Without it the object is an untyped literal, so a typo such as ci: { unitTest: true } stays silent in your editor.

The CLI shape-checks the config against the manifest when it loads it, so a sync still works without the helper. The helper is what moves that feedback into your editor.

Import NuxtBaseConfig directly when you need the type on its own:

import type { NuxtBaseConfig } from "@sidebase/base-config/config";

Shared tsconfig

@sidebase/base-config/tsconfig.base is the shared strict TypeScript base, exported as a JSON file so a tsconfig.json can extend it by package name. The nuxt-app preset ships a managed tsconfig.json that chains it with Nuxt's generated config:

{
  "extends": [
    "@sidebase/base-config/tsconfig.base",
    "./.nuxt/tsconfig.json"
  ]
}

Order matters: the shared base comes first, so Nuxt's generated config applies over it.

Naming convention

Two rules cover every exported name, so you can predict them:

  • Types describing this payload's config shape are NuxtBase*: NuxtBaseConfig, NuxtBaseCiConfig, defineNuxtBaseConfig, and the NUXT_BASE_*_KEYS lists. The prefix is doing real work here, because these types describe the config of this specific payload rather than anything generic.
  • Everything else is domain-named with no package prefix: buildPrismaConfig, applyPrismaDevEnv, createSidebaseEslint, resolveEslintOptions. The import path already states where a symbol comes from, so repeating it in the identifier adds nothing.

streamctl is reserved for the CLI (@sidebase/streamctl) and the files it owns: streamctl.config.ts in the repo root, and the legacy .streamctl/ directory it still reads. Nothing exported from this package carries that name, because nothing exported from this package comes from the CLI. The ESLint layers this package builds are named sidebase/* (sidebase/defaults, sidebase/console, and so on), which is what you see in lint output and what you target if you .override() a layer by name. They are built at runtime by this package; the CLI never sees them.

ESLint factory

@sidebase/base-config/eslint exports createSidebaseEslint(options?), which wraps @antfu/eslint-config (a dependency of this package, so do not add @antfu/eslint-config directly) and returns its FlatConfigComposer, so you chain .append() / .override() for local rules:

// eslint.config.ts
import { createSidebaseEslint } from "@sidebase/base-config/eslint";

export default createSidebaseEslint({ zod: "full", trpcGuard: true })
  .append({ rules: { "vue/multi-word-component-names": "off" } });

Options

| Option | Default | Effect | | ------ | ------- | ------ | | zod | "none" | "full" = ban .extend()/.merge()/.passthrough() + enforce import * as z; "import-style" = import style only; "none" = off | | console | "error" | "error" bans all console.*; an array is the allow-list (e.g. ["warn", "error"]) | | trpcGuard | false | Ban publicProcedure in server/trpc/routers/** | | prismaImportGuard | false | Ban ~~/prisma/ client imports on the app side | | typeDefStyle | "interface" | ts/consistent-type-definitions | | autoImportPaths | ["utils/", "composables/", "~~/shared/types/"] | Paths banned from direct import (Nuxt auto-imports) | | autoImportTypeOnly | [] | Subset of autoImportPaths where import type { ... } stays allowed (value imports still banned) | | ignoresTypeAware | ["prisma.config.ts", "eslint.config.ts", "streamctl.config.ts", ".streamctl/**/*.ts"] | Files excluded from antfu's type-aware program (only relevant when type-aware). Covers both config locations, since .streamctl/config.ts stays supported after the root default lands | | testFilePattern | ["**/*.{test,spec}.{ts,tsx}", "**/*.stories.{ts,tsx}"] | Test/spec/story globs where the process.env + auto-import bans are relaxed |

Rare per-repo rules belong in your local .append(...), not in the option surface.

Rule IDs & inline suppression

The custom bans map to these ESLint rule IDs. Use the exact ID in an // eslint-disable-next-line ... directive to suppress one line:

| Ban | Rule ID | | --- | ------- | | Direct process.env access, as a member expression (process.env.X, const { env } = process) | no-restricted-properties | | ANY import or re-export of node:process or process that can reach env (import { env }, import { env as e }, import * as proc, import proc from "node:process", export { env } from "node:process", export * from "node:process") | ts/no-restricted-imports | | Auto-import path imports (autoImportPaths) | ts/no-restricted-imports | | App-side Prisma client import (prismaImportGuard) | ts/no-restricted-imports | | Zod named-z import style (zod: "import-style"/"full") | no-restricted-syntax | | Zod .extend()/.merge()/.passthrough() (zod: "full") | no-restricted-syntax | | publicProcedure in routers (trpcGuard) | no-restricted-syntax |

"Re-export" is not a figure of speech: a barrel file doing export * from "node:process" reports, as do export { env } from "node:process" and export { default as p } from "node:process". The one shape that does not report is a bare side-effect import with no bindings, import "node:process" -- it cannot reach env, so there is nothing to ban.

The process.env ban is enforced by two rules, and which one reports depends on the shape, so suppressing the wrong one fails twice over: the directive does not suppress anything, and ESLint additionally warns that it was unused.

// eslint-disable-next-line no-restricted-properties
import { env } from 'node:process'

1:1   warning  Unused eslint-disable directive (no problems were reported from 'no-restricted-properties')
2:10  error    'env' import from 'node:process' is restricted...   ts/no-restricted-imports

Requirements

eslint is a peer dependency (^10.5.0), so consumers of /eslint must supply ESLint themselves. The peer range matches the version baseline, so streamctl version-sync keeps a repo's ESLint in step with what this package is tested against.

Flat config only. To load a TypeScript eslint.config.ts on Node, consumers add jiti (>= 2.2.0) as a devDependency: ESLint has no native TS loader, and ESLint 10 rejects jiti below 2.2.0. Writing eslint.config.mjs instead drops the jiti requirement entirely, as does running on Deno or Bun, which import TypeScript directly.

Type-aware linting prerequisite

Type-aware linting is gated behind LINT_TYPEAWARE=true (off by default), so this only matters on the opt-in / CI path. When type-aware lint is on, the Prisma client and Nuxt types must be generated before lint runs, otherwise type-aware rules fail on missing generated types. This is satisfied by ordering, not a preflight check:

  • the managed ci.yml runs prisma generate (and nuxi prepare) before pnpm lint, and
  • scripts.postinstall: "nuxt prepare" regenerates Nuxt types on install.

So a standard pnpm install + the managed CI ordering covers the prerequisite; no separate check is run.

Prisma factory

@sidebase/base-config/prisma exports buildPrismaConfig({ views?, typedSql? }), which returns a Prisma 6.19 config object you wrap in defineConfig:

// prisma.config.ts
import { defineConfig } from "prisma/config";
import { buildPrismaConfig } from "@sidebase/base-config/prisma";

export default defineConfig(buildPrismaConfig({ views: true, typedSql: true }));

It reads DATABASE_URL / DIRECT_DATABASE_URL / SHADOW_DATABASE_URL from the environment (Pattern B): the schema engine gets a pgbouncer-free direct connection: DIRECT_DATABASE_URL if set, otherwise DATABASE_URL with pooler-only query params (pgbouncer, connection_limit, and so on) stripped via ufo. SHADOW_DATABASE_URL is wired only when present; with no database URL the engine block is omitted so the schema's own datasource applies.

| Option | Default | Effect | | ------ | ------- | ------ | | views | false | Configure the views feature (prisma/views) | | typedSql | false | Configure the typedSql preview feature (prisma/sql) |

applyPrismaDevEnv

The same subpath exports applyPrismaDevEnv(env?), the companion for local development. It seeds DIRECT_DATABASE_URL, DATABASE_URL, and SHADOW_DATABASE_URL with localhost defaults, but only when they are unset, so a real .env or shell value always wins. It writes to the environment record you pass and defaults to process.env, which is a deliberate side effect.

You need it because once a prisma.config.ts exists, Prisma stops auto-loading .env. A schema that reads env("DATABASE_URL") then fails with no environment at all. Call it in the config file, after loading dotenv, and the Prisma CLI runs against local postgres with zero setup:

// prisma.config.ts
import "dotenv/config";
import { defineConfig } from "prisma/config";
import { applyPrismaDevEnv, buildPrismaConfig } from "@sidebase/base-config/prisma";

applyPrismaDevEnv();

export default defineConfig(buildPrismaConfig());

buildPrismaConfig alone is enough wherever the connection variables are already set, which is every deployed environment and CI. Add applyPrismaDevEnv when you want prisma migrate dev and friends to work on a fresh checkout without a .env file. The defaults are direct = local postgres, pooled = the direct URL plus pgbouncer=1, and shadow = the direct URL on a /prisma-shadow database.

The URL helpers never log a connection string and never put one in an error message, so a password cannot reach your terminal or CI output through them. Keep that property if you wrap them.

prisma / @prisma/client are peer dependencies (^6.19).

Prisma 7 is out of scope for now. v7 changes the config shape (env() helper, directUrl becomes url, no package.json prisma block); a buildPrismaConfig v7 variant is a deliberate later bump.

Supply-chain cooldown (pnpm-workspace.yaml)

The base preset fully owns pnpm-workspace.yaml, pnpm's settings file. It ships two supply-chain controls, both on by default for every repo:

packages: []
minimumReleaseAge: 10080          # 7 days, in minutes
minimumReleaseAgeExclude:
  - "@sidebase/*"
onlyBuiltDependencies:            # the postinstall-script allowlist
  - "@prisma/client"
  - "esbuild"
  - "prisma"

minimumReleaseAge refuses to resolve a version until it has been published for 7 days, so a compromised release has time to be caught and yanked before the fleet installs it. It needs pnpm 10.16 or newer and gates fresh resolution only, so pnpm install --frozen-lockfile replays the lockfile untouched and CI is unaffected.

The pnpm baseline shipped to consumers is 10.28.1 (presets/*/preset.json). This repo's own packageManager pin tracks separately and is usually newer; the two are independent by design, not drift.

Exclusions match package names, not dependency trees: an excluded package's own dependencies still face the cooldown, and pnpm resolves the newest eligible older version in range. Only when nothing in range is old enough does resolution fail, with a misleading ERR_PNPM_NO_MATCHING_VERSION (pnpm#9998).

@sidebase/* is exempt: the first-party scope covering both this payload and the streamctl CLI, published by the org itself, so a payload release reaches the fleet the same day instead of waiting out its own cooldown.

packages: [] is load-bearing. pnpm defaults it to ** whenever a pnpm-workspace.yaml exists, which would silently promote every nested package.json (test fixtures, examples) to a workspace project and break --frozen-lockfile.

onlyBuiltDependencies is the install-script allowlist. Everything else installs with its lifecycle scripts blocked. Add extras through the config, not through pnpm approve-builds (which writes to the managed file and is reverted on the next sync):

export default {
  // ...
  security: { minimumReleaseAge: "10080" }, // minutes; "0" disables the cooldown
  pnpm: { onlyBuiltDependencies: ["sharp"] }, // on top of the baked-in baseline
};

| Option | Default | Effect | | ------ | ------- | ------ | | security.minimumReleaseAge | "10080" | Minutes a release must age before pnpm installs it. A string, because the manifest's configKeys has no number type, same as the versions.* pins. | | pnpm.onlyBuiltDependencies | [] | Extra packages allowed to run install scripts, appended below the baked-in baseline |

Because the file is full-owned, a repo that already has a pnpm-workspace.yaml raises a one-time adoption conflict on the first sync; reconcile it with streamctl sync --interactive.

Adopting it discards your existing keys. The rendered file is the payload's, so an existing onlyBuiltDependencies allowlist and ignoredBuiltDependencies are both replaced by the baked-in baseline, and nothing warns about it. Most affected packages ship a prebuilt native binary and keep working either way, so the practical breakage is limited to architectures with no prebuild and to postinstalls doing essential non-native work. Capture the old list before you adopt and re-add it via pnpm.onlyBuiltDependencies above. A repo that genuinely needs its own packages: list (a real monorepo) or its own exclude list should opt the file out entirely instead:

files: { "pnpm-workspace.yaml": "off" },

Dockerfile knobs

The nuxt-app preset ships a managed Dockerfile. Beyond docker.aptPackages (an openssl-plus allowlist), six docker.* knobs inject raw text at fixed positions so every fleet repo can express its own build without opting the file out:

| Knob | Position | Default | | ---- | -------- | ------- | | docker.preInstall | build stage, before pnpm install | "" | | docker.buildArgs | build stage, before COPY . . | "" | | docker.buildSteps | the build-command block | nuxi prepare / prisma generate / run build | | docker.finalStage | final stage, before CMD | "" | | docker.prismaRuntime | final stage | copy the schema + install the migrate deploy CLI | | docker.startCommand | inside CMD | prisma migrate deploy then the node server |

export default {
  // ...
  docker: {
    preInstall: "COPY ./vendor ./vendor", // vendored dep needed at install time
    buildSteps: "RUN pnpm nuxi prepare\nRUN pnpm run build", // a repo with no Prisma
    prismaRuntime: "", // drop Prisma entirely
    startCommand: "node .output/server/index.mjs",
  },
};

These six values land verbatim. They are string, not string[], so order is preserved (a sorted array would scramble ordered RUN steps), and no shell-metacharacter check runs on them. The trust level is exactly that of files: { "Dockerfile": "off" }, which any consumer can already set: whoever can edit streamctl.config.ts can already replace the whole file. The USER node switch and the .output ownership stay FIXED outside every knob, so an override cannot re-root the container or drop the runtime user.

Setting a knob replaces its default outright, it does not append. So a docker.buildSteps override must restate every build command you still want, and docker.prismaRuntime: "" removes the Prisma runtime block entirely. The three knobs that default to real content (buildSteps, prismaRuntime, startCommand) are the ones where this matters; the other three default to "".

prismaRuntime and startCommand are coupled -- override one and you must override the other. prismaRuntime is what installs the Prisma CLI into the final stage, and the DEFAULT startCommand is npm exec prisma migrate deploy && node .... Setting only prismaRuntime: "" therefore produces an image whose CMD invokes a CLI that is no longer installed, and the container fails to start. The example above overrides both, which is why it is safe to copy; a single-knob change is not. Drop the migrate deploy half of startCommand at the same time.

Global installs in docker.preInstall must use npm i -g <tool>, not pnpm add -g: the image sets no PNPM_HOME, so pnpm's global bin dir is undefined.

Upgrade-PR workflow

The base preset ships .github/workflows/streamctl-upgrade.yml, a scheduled workflow that opens a PR when a payload update is available. It is an enabledBy-gated managed file, off by default: it only lands once a repo opts in, and it is inert until the required token exists.

Enable it in streamctl.config.ts, then streamctl sync:

export default {
  // ...
  automation: { upgradePr: true },
  // optional: version pins, default to the payload's baseline. `node` feeds
  // ci.yml + the Dockerfile; `pnpm` feeds the Dockerfile only (CI pins pnpm
  // via package.json#packageManager):
  // versions: { node: "24.13.0", pnpm: "10.28.1" },
};

Required secrets:

| Secret | Purpose | | ------ | ------- | | STREAMCTL_PR_TOKEN | A GitHub App installation token or machine-account PAT used to open the PR. Not the default GITHUB_TOKEN: a PR opened with GITHUB_TOKEN does not trigger pull_request workflows, so the repo's own check gate would never run on the bot PR. |

That is the only one, because the payload is public npm, so the install step needs no registry token. A repo whose other dependencies are private must opt this full-owned workflow out and wire its own auth.

The workflow branches on the CLI exit codes: check exit 4 opens a PR; exit 0 stops; exit 3 (pre-existing drift) opens a drift issue instead. A clean upgrade (exit 0) produces a reconcile PR; a conflicted upgrade (exit 2, rolled back) produces a plan-only PR labelled needs-interactive-upgrade for a human to finish with streamctl sync --interactive.

The org bot identity is still being decided. Until the runbook lands, leave the workflow disabled. Design only; nothing is enabled anywhere.

Action pinning policy

Every action in every managed workflow and CI job fragment is pinned by full commit SHA (with the version tag in a trailing comment), since moving tags can be re-pointed, so tags are never trusted, secrets or not. test/workflow-pins.test.ts sweeps all preset templates and fails on any uses: that is not a 40-hex SHA.

The shipped pins track actions/checkout v6, actions/setup-node v6, and pnpm/action-setup v6. These majors run on the node24 action runtime, which needs a GitHub Actions runner >= v2.327.1. GitHub-hosted ubuntu-latest is far past that; only a repo that adds its own self-hosted runner needs to keep it above that floor.