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

render-blueprint

v0.1.0

Published

Typed factories that describe Render resources and synthesize render.yaml

Readme

render-blueprint

render-blueprint describes Render.com resources with typed TypeScript factories and synthesizes them to render.yaml. It is a generator only: it never calls the Render API, and the file it writes is the state.

Install

npm i -D render-blueprint      # npm
pnpm add -D render-blueprint   # pnpm

The package is ESM-only. It needs Node 22.18.0 or newer. On Node 23, it needs 23.6.0 or newer.

Quick start

Put this render.ts at the root of your repository:

import { blueprint, postgres, web } from 'render-blueprint';

const db = postgres('records', { plan: 'basic-1gb' });

const api = web('api', {
  runtime: 'node',
  plan: 'starter',
  buildCommand: 'pnpm install --frozen-lockfile && pnpm build',
  startCommand: 'pnpm start',
  env: { DATABASE_URL: db.connectionString },
});

export default blueprint({ resources: [api, db] });

npx render-blueprint synth writes this render.yaml beside it:

# Generated by render-blueprint. Do not edit this file by hand.
# Edit the TypeScript blueprint that produced it and synthesize again.

services:
  - type: web
    name: api
    plan: starter
    runtime: node
    buildCommand: pnpm install --frozen-lockfile && pnpm build
    startCommand: pnpm start
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: records
          property: connectionString
databases:
  - name: records
    plan: basic-1gb

Command line

npx render-blueprint synth           # write render.yaml
npx render-blueprint check           # compare the committed render.yaml with the blueprint
npx render-blueprint check --strict  # also fail on a warning

The CLI reads the first render.ts, render.mts, render.js or render.mjs from the working directory up. Its default export must be the value of blueprint(...).

Flags follow the command:

  • --file <path> — the blueprint file, instead of the one the walk finds.
  • --out <path> — the YAML file to write or to compare. Defaults to render.yaml beside the blueprint file.
  • --strict — treat a warning as a failure. synth still writes the file, then fails.
  • --help, -h — print the help.
  • --version, -v — print the installed version.

A --file or --out path resolves against the working directory, not the blueprint.

| Exit code | Meaning | | --------- | ----------------------------------------------------------------------------------------------------------------------------- | | 0 | synth wrote the file, or check found the committed file clean. | | 1 | The blueprint is invalid, a file operation failed, the command line was wrong, or --strict turned a warning into a failure. | | 2 | The committed file drifted from the blueprint. |

check compares normalized YAML, so a change in formatting alone is not drift.

References and secrets

import {
  blueprint,
  envGroup,
  external,
  generated,
  literal,
  postgres,
  readReplica,
  secret,
  web,
} from 'render-blueprint';

const replica = readReplica('records-reader');
const db = postgres('records', { readReplicas: [replica] });
const settings = envGroup('settings', { env: { SESSION_SECRET: generated() } });

const api = web('api', {
  runtime: 'node',
  buildCommand: 'pnpm build',
  startCommand: 'pnpm start',
  env: { LOG_FORMAT: 'json' },
});

const admin = web('admin', {
  runtime: 'node',
  buildCommand: 'pnpm build',
  startCommand: 'pnpm start:admin',
  envGroups: [settings],
  env: (self) => ({
    DATABASE_URL: db.connectionString,
    REPLICA_URL: replica.connectionString,
    API_LOG_FORMAT: api.envVar('LOG_FORMAT'),
    PUBLIC_URL: self.renderVar('RENDER_EXTERNAL_URL'),
    LEGACY_AUTH_HOST: external.privateService('legacy-auth').host,
    STRIPE_KEY: secret(),
    LOG_LEVEL: literal('info', { previewValue: 'debug' }),
  }),
});

export default blueprint({ resources: [db, settings, api, admin] });

A handle has only the properties that Render can reference, so a worker has no .host. generated() emits generateValue: true.

Projects and environments

import { blueprint, environment, project, web, worker } from 'render-blueprint';

const api = web('api', { runtime: 'image', image: { url: 'ghcr.io/acme/api:1.4.2' } });
const staging = web('api-staging', { runtime: 'image', image: { url: 'ghcr.io/acme/api:1.5.0' } });
const jobs = worker('jobs', { runtime: 'image', image: { url: 'ghcr.io/acme/jobs:2.0.0' } });

export default blueprint({
  projects: [
    project('acme', {
      environments: [
        environment('production', { resources: [api], permissions: { protection: 'enabled' } }),
        environment('staging', { resources: [staging] }),
      ],
    }),
  ],
  ungrouped: [jobs],
});

A resource goes in exactly one place: resources, one environment, or ungrouped.

Defaults

import { blueprint, withDefaults } from 'render-blueprint';

const acme = withDefaults({
  region: 'oregon',
  repo: 'https://github.com/acme/mono',
  branch: 'main',
  rootDir: 'apps/api',
  autoDeployTrigger: 'checksPass',
  buildFilter: { paths: ['apps/api/**'], ignoredPaths: ['**/*.md'] },
  ipAllowList: [{ source: '203.0.113.4/30', description: 'office' }],
  plan: { web: 'starter', postgres: 'basic-1gb' },
});

const db = acme.postgres('records');

const api = acme.web('api', {
  runtime: 'node',
  plan: 'standard', // wins over plan.web
  buildCommand: 'pnpm install --frozen-lockfile && pnpm build',
  startCommand: 'pnpm start',
  env: { DATABASE_URL: db.connectionString },
});

export default blueprint({ resources: [api, db] });

Each factory on acme takes the values its kind can hold. plan takes one key per kind. A value on a resource always wins. Scopes nest: acme.withDefaults(...) replaces only the values it sets.

What it catches

synth and check report every issue at once on stderr:

$ npx render-blueprint check
The blueprint has 3 validation issues; nothing was synthesized.
  records.diskSizeGB: A database disk size is 1 GB or a multiple of 5 GB, and Render never shrinks one.
  nightly.schedule: A schedule is a cron expression of five fields — minute, hour, day of month, month and day of week, as in "0 2 * * *" — and "every night" is not one.
  api.name: More than one resource is named "api". Render identifies a resource by its name, so every name in a blueprint must be unique.
$ echo $?
1

With previews on at the root, secret() warns on stderr:

warning api.env.STRIPE_KEY: "api" sets "STRIPE_KEY" with secret() while previews are on. Render does not copy a sync: false variable into a preview environment, so declare it in an environment group the Dashboard manages instead.

synth still prints the result on stdout and exits 0:

Wrote /home/acme/mono/render.yaml

src/validation/issue.ts lists every code.

Names in TypeScript and names in YAML

Every field keeps Render's own name, except these nine.

| TypeScript | YAML | | ----------------------------------------------------------------- | ----------------------------------------------- | | privateService(…) | type: pserv | | staticSite(…) | type: web + runtime: static | | keyValue(…) | type: keyvalue | | env: { KEY: value } | envVars: [{ key, … }] | | envGroups: [g] | envVars: [{ fromGroup }] | | instances | numInstances | | secret() | sync: false | | previews.plan / previews.diskSizeGB on Postgres and Key Value | previewPlan / previewDiskSizeGB | | extraFields | merged into the mapping, and not a Render field |

Testing

memoryFilePort() keeps files in memory, so a test writes nothing to disk.

import assert from 'node:assert/strict';
import { test } from 'node:test';

import { blueprint, checkBlueprint, web, writeBlueprint } from 'render-blueprint';
import { memoryFilePort } from 'render-blueprint/testing';

const value = blueprint({
  resources: [web('api', { runtime: 'image', image: { url: 'ghcr.io/acme/api:1.4.2' } })],
});

test('render.yaml matches the blueprint', async () => {
  const port = memoryFilePort();
  await writeBlueprint(value, { path: 'render.yaml', port });
  const drift = await checkBlueprint(value, { path: 'render.yaml', port });

  assert.equal(drift.unwrap().status, 'clean');
  assert.match(port.files.get('render.yaml') ?? '', /name: api/);
});

Programmatic API

synthesize returns a better-result Result. writeBlueprint and checkBlueprint return a Promise of one.

| Export | What it does | | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | synthesize(value) | Validates the blueprint, then returns the YAML text and the warnings. | | writeBlueprint(value, { path, port }) | Synthesizes the blueprint, then writes the file at path. | | checkBlueprint(value, { path, port }) | Synthesizes the blueprint, then compares it with the file at path. | | nodeFilePort | The default port: the real filesystem. | | BlueprintInvalid | The error that carries every validation issue. | | DriftReport | The report of checkBlueprint: status: 'clean', or status: 'drift' with a diff and the immutable fields that changed. |

Gotchas

  • Import another blueprint file with its .ts extension. For tsc, set allowImportingTsExtensions with noEmit, or rewriteRelativeImportExtensions.
  • Node strips the types, so use erasable syntax only, for example: no enum, no parameter properties, no namespace.
  • Without "type": "module" in package.json, Node warns MODULE_TYPELESS_PACKAGE_JSON. Set the field, or use .mts for every blueprint file.

Docs

Develop

pnpm install
git config core.hooksPath .githooks   # once per clone: the commit hooks
pnpm check                            # format, lint, typecheck, tests, type tests
pnpm build                            # the ESM bundle, the types and the bin, into dist/

License

MIT