Skip to content

Latest commit

 

History

History
430 lines (311 loc) · 11.9 KB

File metadata and controls

430 lines (311 loc) · 11.9 KB

Exported Functions

Everything you can import and call. Each one does exactly what the name says, which is more than I can say for most npm packages.

check(options)

The main event. Loads packages, resolves dependencies against the registry, renders output, and optionally writes updates. Everything the CLI does, minus the argument parsing.

function check(options: depfreshOptions): Promise<number>

Returns an exit code:

  • 0 -- no updates found, or updates were written successfully
  • 1 -- updates available (only when failOnOutdated: true and write: false)
  • 2 -- something went wrong, or strict failure flags tripped (failOnResolutionErrors, failOnNoPackages, strictPostWrite)
import { check, resolveConfig } from 'depfresh'

const options = await resolveConfig({ mode: 'latest', write: true })
const code = await check(options)

if (code === 2) {
  console.error('Well that did not work')
}

resolveConfig(overrides?)

Merges your overrides with config file values (.depfreshrc, depfresh.config.ts, package.json#depfresh) and DEFAULT_OPTIONS. Config file wins over defaults. Your overrides win over everything.

function resolveConfig(overrides?: Partial<depfreshOptions>): Promise<depfreshOptions>

Config resolution order (highest priority first):

  1. overrides argument
  2. Config file (.depfreshrc, depfresh.config.ts, depfresh.config.js)
  3. package.json "depfresh" field
  4. DEFAULT_OPTIONS
import { resolveConfig } from 'depfresh'

// Just defaults + config file
const options = await resolveConfig()

// Override specific options
const options = await resolveConfig({
  cwd: '/path/to/project',
  mode: 'patch',
  concurrency: 4,
})

defineConfig(options)

A type helper for config files. Does literally nothing at runtime. Returns exactly what you pass in. But your editor will autocomplete, and that's apparently worth an import.

function defineConfig(options: Partial<depfreshOptions>): Partial<depfreshOptions>
// depfresh.config.ts
import { defineConfig } from 'depfresh'

export default defineConfig({
  mode: 'minor',
  exclude: ['typescript'],
  group: true,
})

loadPackages(options)

Finds and parses package manifests (package.json, package.yaml) in your project. Respects recursive, ignorePaths, ignoreOtherWorkspaces. Loads workspace catalogs (pnpm, bun, yarn) only when recursive: true, and supports global packages when global: true (single detected manager) or globalAll: true (npm + pnpm + bun).

When both package.yaml and package.json exist in a directory, package.yaml is selected.

function loadPackages(options: depfreshOptions): Promise<PackageMeta[]>
import { loadPackages, resolveConfig } from 'depfresh'

const options = await resolveConfig({ recursive: true })
const packages = await loadPackages(options)

for (const pkg of packages) {
  console.log(`${pkg.name}: ${pkg.deps.length} dependencies`)
}

resolvePackage(pkg, options, externalCache?, externalNpmrc?, privatePackages?)

Resolves every dependency in a package against the registry. Handles caching, concurrency, version filtering, workspace protocol semantics, and the onDependencyResolved callback. This is where the network calls happen.

function resolvePackage(
  pkg: PackageMeta,
  options: depfreshOptions,
  externalCache?: Cache,
  externalNpmrc?: NpmrcConfig,
  privatePackages?: Set<string>,
): Promise<ResolvedDepChange[]>
Param Description
pkg The package to resolve
options Full depfresh options (mode, concurrency, timeout, etc.)
externalCache? Reuse a cache across multiple packages. If omitted, creates and closes its own
externalNpmrc? Pre-loaded npmrc config. If omitted, loads from disk
privatePackages? Set of workspace package names normally skipped for local-only refs. Explicit-version workspace: refs still resolve against the registry.
import { loadPackages, resolvePackage, resolveConfig } from 'depfresh'

const options = await resolveConfig({ mode: 'latest' })
const packages = await loadPackages(options)

for (const pkg of packages) {
  const resolved = await resolvePackage(pkg, options)
  const updates = resolved.filter(d => d.diff !== 'none')
  console.log(`${pkg.name}: ${updates.length} updates available`)
}

parseDependencies(raw, options)

Extracts dependencies from a parsed package manifest object (JSON or YAML). Handles all standard fields, packageManager, overrides, resolutions, nested overrides, protocols (npm:, jsr:, github:, workspace:), include/exclude filters, and locked version detection.

function parseDependencies(
  raw: Record<string, unknown>,
  options: depfreshOptions,
): RawDep[]
import { parseDependencies, resolveConfig } from 'depfresh'

const options = await resolveConfig()
const raw = JSON.parse(fs.readFileSync('package.json', 'utf-8')) // package.yaml works too after YAML parsing
const deps = parseDependencies(raw, options)

console.log(`Found ${deps.length} dependencies`)
console.log(`${deps.filter(d => d.update).length} will be checked`)

writePackage(pkg, changes, loglevel?)

Writes resolved changes back to the package file. Preserves indentation, line endings (CRLF too, you're welcome Windows users), and key order. Handles regular manifest files (package.json, package.yaml) and workspace catalog files.

function writePackage(
  pkg: PackageMeta,
  changes: ResolvedDepChange[],
  loglevel?: 'silent' | 'info' | 'debug',
): void
import { loadPackages, resolvePackage, writePackage, resolveConfig } from 'depfresh'

const options = await resolveConfig({ mode: 'minor' })
const [pkg] = await loadPackages(options)

const resolved = await resolvePackage(pkg, options)
const minorOnly = resolved.filter(d => d.diff === 'minor' || d.diff === 'patch')

writePackage(pkg, minorOnly, 'silent')

loadGlobalPackages(pm?)

Lists globally installed packages for one package manager. Auto-detects which PM is available if you don't specify (tries pnpm, then bun, then npm).

function loadGlobalPackages(pm?: string): PackageMeta[]
import { loadGlobalPackages } from 'depfresh'

const packages = loadGlobalPackages('npm')
for (const pkg of packages) {
  for (const dep of pkg.deps) {
    console.log(`${dep.name}@${dep.currentVersion}`)
  }
}

loadGlobalPackagesAll()

Lists globally installed packages across npm, pnpm, and bun in one pass. Results are deduplicated by package name. If a package exists in multiple managers, write mode targets every matching manager.

function loadGlobalPackagesAll(): PackageMeta[]
import { loadGlobalPackagesAll } from 'depfresh'

const packages = loadGlobalPackagesAll()
for (const dep of packages[0]?.deps ?? []) {
  console.log(`${dep.name}@${dep.currentVersion}`)
}

writeGlobalPackage(pm, name, version)

Updates a single global package. Shells out to the relevant package manager's install command. Not subtle.

function writeGlobalPackage(
  pm: PackageManagerName,
  name: string,
  version: string,
): void
import { writeGlobalPackage } from 'depfresh'

writeGlobalPackage('npm', 'typescript', '5.7.0')
// Runs: npm install -g typescript@5.7.0

Lifecycle Callbacks

Seven hooks. Called in order. All optional. All async-compatible. Wire them into depfreshOptions and check() will call them at the right moment.

If you need reusable behavior across projects, use options.addons with depfreshAddon objects. Callbacks stay project-local; addons are composable plugins.

Call Order

check() starts
  |
  +- loadPackages()
  |
  +- afterPackagesLoaded(pkgs)
  |
  +- for each package:
  |   +- beforePackageStart(pkg)
  |   +- resolvePackage()
  |   |   +- onDependencyResolved(pkg, dep)  <- called per dep as it resolves
  |   +- afterPackageEnd(pkg)
  |   |
  |   +- if writing:
  |       +- beforePackageWrite(pkg)  <- return false to skip
  |       +- afterPackageWrite(pkg, changes)
  |
  +- afterPackagesEnd(pkgs)

afterPackagesLoaded(pkgs)

Called once after all packages have been discovered and parsed, before any resolution starts. Good for filtering, logging, or questioning your life choices.

afterPackagesLoaded?: (pkgs: PackageMeta[]) => void | Promise<void>

beforePackageStart(pkg)

Called before each package starts resolving. The pkg.deps array is populated but pkg.resolved is still empty.

beforePackageStart?: (pkg: PackageMeta) => void | Promise<void>

onDependencyResolved(pkg, dep)

Called as each individual dependency finishes resolving. This is your streaming hook -- build a progress bar, update a UI, send a webhook, whatever keeps you entertained.

onDependencyResolved?: (pkg: PackageMeta, dep: ResolvedDepChange) => void | Promise<void>

afterPackageEnd(pkg)

Called after each package is fully resolved (and optionally written). pkg.resolved is now populated.

afterPackageEnd?: (pkg: PackageMeta) => void | Promise<void>

beforePackageWrite(pkg)

Called before writing changes to disk. Return false to skip this package. Return true (or nothing) to proceed. This is your last chance to prevent regrettable decisions.

beforePackageWrite?: (pkg: PackageMeta) => boolean | Promise<boolean>

afterPackageWrite(pkg, changes)

Called after the file has been written. The damage is done. Use this for logging, notifications, or post-write commands. Receives the package and the list of changes that were applied.

afterPackageWrite?: (pkg: PackageMeta, changes: ResolvedDepChange[]) => void | Promise<void>

afterPackagesEnd(pkgs)

Called once after all packages have been processed. The final callback. End of the line.

afterPackagesEnd?: (pkgs: PackageMeta[]) => void | Promise<void>

Callback Examples

Streaming progress:

let resolved = 0
let total = 0

const options = await resolveConfig({
  mode: 'latest',
  afterPackagesLoaded(pkgs) {
    total = pkgs.reduce((sum, p) => sum + p.deps.filter(d => d.update).length, 0)
    console.log(`Checking ${total} dependencies across ${pkgs.length} packages...`)
  },
  onDependencyResolved(_pkg, dep) {
    resolved++
    process.stdout.write(`\r[${resolved}/${total}] ${dep.name}`)
  },
  afterPackagesEnd() {
    process.stdout.write('\n')
    console.log('Done.')
  },
})

await check(options)

Addons

Addons are executed in array order (options.addons). For each lifecycle event, depfresh calls:

  1. The legacy callback (if present)
  2. Each addon hook in order

For beforePackageWrite, returning false from any callback/addon skips writing that package.

import { check, resolveConfig, type depfreshAddon } from 'depfresh'

const metricsAddon: depfreshAddon = {
  name: 'metrics',
  setup(ctx) {
    console.log(`run ${ctx.runId} started at ${ctx.startedAt.toISOString()}`)
  },
  afterPackageWrite(_ctx, pkg, changes) {
    console.log(`${pkg.name}: ${changes.length} updates written`)
  },
}

const options = await resolveConfig({
  write: true,
  addons: [metricsAddon],
})

await check(options)

Built-in Addons

depfresh ships with one built-in addon:

import { createVSCodeAddon } from 'depfresh'

const options = await resolveConfig({
  write: true,
  addons: [createVSCodeAddon()],
})

await check(options)

createVSCodeAddon() syncs the engines.vscode field with the @types/vscode version when writing updates. Niche, but if you're building VS Code extensions, it saves a manual step.

Conditional write -- skip specific packages:

const options = await resolveConfig({
  write: true,
  mode: 'minor',
  beforePackageWrite(pkg) {
    // Never auto-update the root package
    if (pkg.name === 'my-monorepo-root') {
      console.log(`Skipping ${pkg.name}`)
      return false
    }
    return true
  },
  afterPackageWrite(pkg, changes) {
    console.log(`Updated ${pkg.filepath} (${changes.length} changes)`)
  },
})

await check(options)