packall

@packall/cli

The command itself — the parser, the surface as data, and the reporting.

import { … } from "@packall/cli";

Services and classes

Npmrc

Service tag for .npmrc resolution.

class Npmrc {
  readonly Service: {
     readonly load: (explicitPath: string | undefined) => Effect<LoadedNpmrc, never, FileSystem | Path>;
  }
  readonly key: "@packall/cli/Npmrc"
}

Npmrc.Service

type Npmrc.Service = {
   readonly load: (explicitPath: string | undefined) => Effect<LoadedNpmrc, never, FileSystem | Path>;
}

ProgressRenderer

Consumes progress events and renders them.

class ProgressRenderer {
  new (options: {
     readonly interactive: boolean;
     readonly quiet: boolean;
     readonly write: (text: string) => void;
     readonly now?: (() => number) | undefined;
     readonly columns?: number | undefined;
     readonly frameIntervalMs?: number | undefined;
     readonly tickIntervalMs?: number | undefined;
     readonly preflightTimeoutMs?: number | undefined;
     readonly ticker?: Ticker | undefined;
  }): ProgressRenderer
  readonly options: Required<Omit<Options, "now" | "write">> & {
     readonly write: (text: string) => void;
     readonly now: () => number;
  }
  readonly phase: "" | Phase
  readonly phaseStartedAt: number
  readonly lastFrameAt: number
  readonly lineIsDirty: boolean
  readonly ticks: number
  readonly stopTicking: (() => void) | undefined
  readonly state: {
     readonly resolved: number;
     readonly downloaded: number;
     readonly downloadTotal: number;
     readonly bytes: number;
     readonly current: string;
  }
  readonly warnings: string[]
  readonly collectedWarnings: readonly string[]
  readonly handle: (event: ProgressEvent) => void
  readonly finish: () => void
  readonly tick: () => void
  readonly startTicker: (phase: Phase) => void
  readonly stopTicker: () => void
  readonly announcePhase: (phase: Phase, total: number | undefined) => void
  readonly summarizePhase: (phase: Phase) => void
  readonly draw: (force?: boolean) => void
  readonly frame: () => string
  readonly endLine: () => void
  readonly log: (line: string) => void
}

ProgressRenderer.Ticker

Drives the animation between events, returning a function that stops it.

Injected rather than called directly so a test can step the spinner by hand; the default is a setInterval that is unref'd, since an animation must never be the reason a finished process is still running.

type ProgressRenderer.Ticker = (tick: () => void, intervalMs: number) => () => void

ProgressRenderer.Options

type ProgressRenderer.Options = {
   readonly interactive: boolean;
   readonly quiet: boolean;
   readonly write: (text: string) => void;
   readonly now?: (() => number) | undefined;
   readonly columns?: number | undefined;
   readonly frameIntervalMs?: number | undefined;
   readonly tickIntervalMs?: number | undefined;
   readonly preflightTimeoutMs?: number | undefined;
   readonly ticker?: Ticker | undefined;
}

Transport

Service tag for the HTTP backend.

class Transport {
  readonly Service: {
     readonly layerFor: (options: Options) => Layer<HttpClient, never, never>;
     readonly canProbeRoot: boolean;
  }
  readonly key: "@packall/cli/Transport"
}

Transport.Options

Everything that has to be settled before a transport can exist.

Both members are here for the same reason: they are properties of the connection, not of a request, so neither can be applied after the fact by a caller holding a client. tls in particular is the whole reason this takes an object — strict-ssl and cafile in an .npmrc are worth nothing unless they reach the thing that opens the socket.

type Transport.Options = { readonly requestTimeoutMs: number; readonly tls: TlsSettings; }

Transport.Service

The HTTP backend.

type Transport.Service = {
   readonly layerFor: (options: Options) => Layer<HttpClient, never, never>;
   readonly canProbeRoot: boolean;
}

Tty

Service tag for the host.

class Tty {
  readonly Service: {
     readonly isInteractive: boolean;
     readonly canPrompt: boolean;
     readonly platform: string;
     readonly columns: number;
     readonly write: (text: string) => void;
     readonly writeOut: (text: string) => void;
     readonly exit: (code: number) => void;
     readonly cwd: string;
     readonly path: Path;
  }
  readonly key: "@packall/cli/Tty"
}

Tty.Service

The host the CLI is attached to.

type Tty.Service = {
   readonly isInteractive: boolean;
   readonly canPrompt: boolean;
   readonly platform: string;
   readonly columns: number;
   readonly write: (text: string) => void;
   readonly writeOut: (text: string) => void;
   readonly exit: (code: number) => void;
   readonly cwd: string;
   readonly path: Path;
}

Functions

announcePlatform

The one reader of process that a service cannot be put in front of.

Effect's prompts choose between unicode and ASCII glyphs by reading process.platform themselves, in an Effect.sync, with no typeof guard — and there is nothing to intercept it with: Prompt.Environment is FileSystem | Path | Terminal, and the glyphs are not an option on the prompt. So in a page the first question the CLI asks dies on a bare ReferenceError: process is not defined, after the resolution it was asking about has already been paid for.

Every host layer calls this while it is being built, which is why the answer is a field on the service rather than a constant here: only the layer knows which host this is, and there is exactly one layer per environment. Node finds the question already answered and this leaves it alone. A browser has no process to leave alone, so it gets the smallest object that answers this one question — nothing else about it is true, and nothing else should look true either.

A build-time define in the page's bundler would also silence the crash, and was rejected: it puts the answer somewhere no reader of this package would think to look, and it is per-bundle rather than per-host, so a second embedder inherits the bug.

const announcePlatform: (platform: string) => void

availablePlatforms

The os-arch pairs the resolved packages declare.

Negations and wildcards are skipped — a package saying "not win32" tells you nothing about which platforms exist to choose between.

const availablePlatforms: (resolution: Resolution) => readonly string[]

bar

A fixed-width progress bar.

Uses block-drawing characters, which every terminal that reports itself as a TTY has handled for a decade.

const bar: (fraction: number, width?: number) => string

bytes

1536 -> "1.5 KB". Decimal units, because that is what registries report.

const bytes: (value: number) => string

chooseLayout

const chooseLayout: (resolution: Resolution, config: Config, current: Layout, interactive: boolean) => Effect<Layout, never, Environment>

choosePlatforms

Offers the platforms this resolution contains, and narrows to the chosen set.

Only when --interactive is on, --platform was not already given, and optional dependencies are being followed — there is nothing to ask about otherwise. Answering with everything selected is the same as not narrowing.

const choosePlatforms: (resolution: Resolution, config: Config, current: PlatformFilter, interactive: boolean) => Effect<PlatformFilter, never, Environment>

collectInput

Merges positional specs with anything from --file, and settles pinning.

const collectInput: (config: Config, interactive: boolean) => Effect<ResolvedInput, Error, Tty | Environment>

decideLayout

const decideLayout: (input: LayoutInput) => LayoutDecision

duration

93000 -> "1m 33s".

Duration.format does the decomposition — there is no reason to reimplement "how many minutes are in 93 seconds" — but it renders every non-zero part it finds, so 3,700,000 ms comes out as 1h 1m 40s. Two units is as much as a progress line can spend on a number nobody is reading precisely, and the seconds place on an hour-long run is noise that changes every frame. So the value is quantised first and formatted after.

Sub-second values keep their own branch: Duration.format renders zero as "0", and Done in 0 reads like a bug.

const duration: (ms: number) => string

equivalentCommand

The command that reproduces this run non-interactively.

Printed after anything was decided by a prompt, and on every --dry-run. Exploring is how you find the right flags; this is what lets you keep them — paste it into a script, a ticket, or a CI job and get the identical bundle without answering a single question.

Only non-default values are emitted, so the line stays short enough to read.

const equivalentCommand: (echo: CommandEcho, tty: Service) => string

eta

Estimates remaining time from a completion rate.

const eta: (completed: number, total: number, elapsedMs: number) => string | undefined

fit

Pads or truncates to exactly width, for stable single-line redraws.

const fit: (value: string, width: number) => string

formatError

Renders a failure.

Our own errors already carry a full, actionable message — including the suggested .npmrc line for an auth failure and the host name for an unreachable registry — so the job here is mostly to not get in the way.

const formatError: (error: unknown) => string

importerPath

Where the package.json sits relative to the lockfile's directory.

Separators are normalised because the lockfile formats all spell their workspace keys POSIX-style regardless of the platform that wrote them.

const importerPath: (lockfileDir: string, packageJsonDir: string, path: Path) => string

lockfileCandidates

The paths a policy would consider for a package.json, nearest first.

The walk up the directory tree is what makes this useful in a monorepo, where packages/core/package.json has no lockfile of its own and the one that pins it sits at the repository root. Each level records how to get back down, so the parse can scope to that member rather than bundling every sibling's dependencies too.

Pure, and exported so the ordering is testable without a filesystem.

const lockfileCandidates: (policy: LockfilePolicy, directory: string, path: Path) => readonly LockfileCandidate[]

parseForce

Reads --force, which is deliberately overloaded.

true / false / prompt are the whole-run policies. Anything else is read as a spec: --force tsdown esbuild@0.21.5 replaces exactly those two and still asks about the rest, which is what you want in a directory holding a dozen bundles when only one is stale.

Separators are commas or whitespace, for the same PowerShell reason as --platform.

const parseForce: (force: Option<boolean>, spec: readonly string[]) => ForcePolicy

parseLockfilePolicy

Reads --lockfile.

Overloaded the same way --force is, and for the same reason: the values are a small closed set of policies plus one open-ended case, and splitting them across two flags would make the common form longer without making anything clearer. Anything unrecognised is a path, so a lockfile called ci/locked.json needs no extra ceremony.

const parseLockfilePolicy: (value: string) => LockfilePolicy

parsePlatforms

Parses --platform win32,linux, --platform "win32 linux", and --platform win32 --platform linux — all to the same thing.

Whitespace counts as a separator because PowerShell turns an unquoted win32,linux into the single argument "win32 linux": a comma builds an array literal there, and arrays are space-joined on their way to a native command. Accepting that costs nothing — no platform token contains a space — and it means the obvious command works in the shell most of these users are sitting in, rather than failing with a confusing quoting error.

const parsePlatforms: (inputs: readonly string[]) => Effect<PlatformFilter, Error, never>

pluralize

pluralize(1, "package") -> "1 package".

const pluralize: (count: number, singular: string, plural?: string) => string

quoteArgument

Shell-quotes an argument.

Specs beginning with @ are quoted unconditionally: PowerShell reads a leading @ as the array/splat operator, so npmb @babel/core is a parse error there. A quoted spec is correct in every shell involved, and a copy-pasteable command that only works in bash is not much use to somebody on Windows — which, for this tool, is most people.

const quoteArgument: (argument: string) => string

relativeToCwd

Renders an absolute path the way you would have typed it.

Flag.directory and Flag.file resolve their values before the handler sees them, which is right for doing the work and wrong for showing it back: an echoed command reading --out ./temp is what you meant, and it stays correct when pasted somewhere else in the same tree.

Paths outside the working directory keep their absolute form — a ../../.. chain is longer than what it replaces and silently depends on where you run it. Separators are normalised to /, which every shell on Windows accepts.

The host arrives as a value rather than being read from process, so this is testable without chdir'ing the test process — and so the identical function renders virtual paths in a browser, where there is no process to read.

const relativeToCwd: (target: string, tty: Service) => string

resolveOverwrites

const resolveOverwrites: (resolution: Resolution, options: BundleContext, config: Config, interactive: boolean) => Effect<OverwritePlan, never, Environment>

seconds

4200 -> "4s".

Whole seconds, unlike duration, because this is for a counter that is redrawn ten times a second: rounding to the nearest millisecond would make every frame differ, and a number that never stops changing is harder to read than one that ticks once a second.

const seconds: (ms: number) => string

specsWithLockfileError

Explains why positional specs and a lockfile cannot be one run.

Both ways out are given as commands rather than described, because the useful thing at this point is the line to run next — the same reason --dry-run prints the command that reproduces it.

Which way out comes first depends on how the lockfile got here. --lockfile off governs detection, so it un-pins a file that was found and does nothing at all to one that was named with --file; offering it there would send you round the same error a second time.

const specsWithLockfileError: (config: Pick<Config, "out" | "specs">, lockfilePath: string | undefined, filePath: string | undefined, tty: Service) => Error

toJson

The machine-readable summary.

const toJson: (result: BundleResult, options: BundleContext, elapsedMs: number) => string

toText

The human-readable summary.

const toText: (input: TextInput) => string

truncateStart

Truncates to width, keeping the end of the string.

Package names are most distinctive at the tail (@babel/plugin-transform-… tells you nothing; …-modules-commonjs tells you everything), so the head is what gets dropped.

const truncateStart: (value: string, width: number) => string

Constants

bundleCommand

The root command.

const bundleCommand: Command<"packall", {
   readonly file: Option<string>;
   readonly prod: boolean;
   readonly lockfile: string;
   readonly out: string;
   readonly layout: Option<"dir" | "flat" | "per-spec" | "single">;
   readonly layoutThreshold: Option<number>;
   readonly archiveName: Option<string>;
   readonly allVersions: boolean;
   readonly maxVersions: Option<number>;
   readonly includePrerelease: boolean;
   readonly optional: boolean;
   readonly peer: boolean;
   readonly platform: readonly string[];
   readonly registry: Option<string>;
   readonly npmrc: Option<string>;
   readonly rewriteTarballHost: boolean;
   readonly timeout: number;
   readonly retries: number;
   readonly concurrency: number;
   readonly dryRun: boolean;
   readonly verify: boolean;
   readonly force: Option<boolean>;
   readonly forceSpec: readonly string[];
   readonly printAbsolutePath: boolean;
   readonly json: boolean;
   readonly quiet: boolean;
   readonly interactive: boolean;
   readonly yes: boolean;
   readonly specs: readonly unknown[];
}, {
}, never, Archiver | Crypto | Npmrc | Transport | Tty | Environment>

cli

The runnable CLI.

const cli: Effect<void, CliError, Archiver | Crypto | Npmrc | Transport | Tty | Environment>

DEFAULT_LAYOUT_PROMPT_THRESHOLD

Above this many archives, per-spec means importing a lot of separate tarballs, which is worth a second thought before writing them.

const DEFAULT_LAYOUT_PROMPT_THRESHOLD: 20

descriptors

const descriptors: {
   readonly file: {
     readonly name: "file";
     readonly alias: "f";
     readonly metavar: "PATH";
     readonly description: "Read specs from a file: a package.json (bundles its dependencies), a lockfile (bundles the versions it pins), or one spec per line with # and // comments. Detected by content, not extension";
     readonly group: "input";
     readonly type: {
       readonly _tag: "File";
       readonly mustExist: true;
    };
  };
   readonly prod: {
     readonly name: "prod";
     readonly description: "With --file <package.json>, skip devDependencies";
     readonly group: "input";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: false;
    };
     readonly dependsOn: {
       readonly flag: "file";
    };
  };
   readonly lockfile: {
     readonly name: "lockfile";
     readonly metavar: "MODE";
     readonly description: "With --file <package.json>: detect finds a sibling lockfile (default) · off never does · npm, pnpm or bun requires that one · or pass a path";
     readonly group: "input";
     readonly type: {
       readonly _tag: "Text";
       readonly default: "detect";
    };
  };
   readonly out: {
     readonly name: "out";
     readonly alias: "o";
     readonly metavar: "DIR";
     readonly description: "Directory to write bundles into (default: the current directory)";
     readonly group: "output";
     readonly type: {
       readonly _tag: "Directory";
       readonly default: ".";
    };
  };
   readonly layout: {
     readonly name: "layout";
     readonly description: string;
     readonly group: "output";
     readonly type: {
       readonly _tag: "Choice";
       readonly choices: readonly Layout[];
    };
  };
   readonly layoutThreshold: {
     readonly name: "layout-threshold";
     readonly metavar: "N";
     readonly description: string;
     readonly group: "output";
     readonly type: {
       readonly _tag: "Integer";
       readonly default: undefined;
    };
  };
   readonly archiveName: {
     readonly name: "archive-name";
     readonly metavar: "NAME";
     readonly description: "Base name for the archive in --layout single";
     readonly group: "output";
     readonly type: {
       readonly _tag: "Text";
       readonly default: undefined;
    };
     readonly dependsOn: {
       readonly flag: "layout";
       readonly value: "single";
    };
  };
   readonly allVersions: {
     readonly name: "all-versions";
     readonly description: "Bundle every published version matching a range, not just the best match";
     readonly group: "selection";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: false;
    };
  };
   readonly maxVersions: {
     readonly name: "max-versions";
     readonly metavar: "N";
     readonly description: "With --all-versions, keep at most the N newest matching versions";
     readonly group: "selection";
     readonly type: {
       readonly _tag: "Integer";
       readonly default: undefined;
    };
     readonly dependsOn: {
       readonly flag: "all-versions";
       readonly value: true;
    };
  };
   readonly includePrerelease: {
     readonly name: "include-prerelease";
     readonly description: "Let prereleases satisfy ranges";
     readonly group: "selection";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: false;
    };
  };
   readonly optional: {
     readonly name: "optional";
     readonly description: "Follow optionalDependencies. On by default — this is where per-platform native binaries live";
     readonly group: "selection";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: true;
    };
  };
   readonly peer: {
     readonly name: "peer";
     readonly description: "Follow non-optional peerDependencies";
     readonly group: "selection";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: true;
    };
  };
   readonly platform: {
     readonly name: "platform";
     readonly metavar: "LIST";
     readonly description: "Restrict optional deps to these platforms. An OS alone takes every arch: win32,linux · linux-x64 · linux-x64-musl (default: all)";
     readonly group: "selection";
     readonly type: {
       readonly _tag: "TextList";
    };
  };
   readonly registry: {
     readonly name: "registry";
     readonly alias: "r";
     readonly metavar: "URL";
     readonly description: "Registry to fetch from. Outranks every other source, including npm_config_registry (default: your .npmrc)";
     readonly group: "registry";
     readonly type: {
       readonly _tag: "Text";
       readonly default: undefined;
    };
  };
   readonly npmrc: {
     readonly name: "npmrc";
     readonly metavar: "PATH";
     readonly description: "Explicit .npmrc to read registries and credentials from. npm_config_registry in the environment still outranks its registry= line";
     readonly group: "registry";
     readonly type: {
       readonly _tag: "File";
       readonly mustExist: true;
    };
  };
   readonly rewriteTarballHost: {
     readonly name: "rewrite-tarball-host";
     readonly description: "Fetch tarballs from --registry rather than the URL the registry returned";
     readonly group: "registry";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: false;
    };
  };
   readonly timeout: {
     readonly name: "timeout";
     readonly metavar: "MS";
     readonly description: "Ceiling on every network wait: connecting, the preflight check, and each request. Prevents an unreachable registry hanging the run";
     readonly group: "registry";
     readonly type: {
       readonly _tag: "Integer";
       readonly default: 10000;
    };
  };
   readonly retries: {
     readonly name: "retries";
     readonly metavar: "N";
     readonly description: "Retries for transient failures (5xx, 429, dropped connections)";
     readonly group: "registry";
     readonly type: {
       readonly _tag: "Integer";
       readonly default: 3;
    };
  };
   readonly concurrency: {
     readonly name: "concurrency";
     readonly alias: "c";
     readonly metavar: "N";
     readonly description: "Simultaneous registry requests";
     readonly group: "registry";
     readonly type: {
       readonly _tag: "Integer";
       readonly default: 10;
    };
  };
   readonly dryRun: {
     readonly name: "dry-run";
     readonly description: "Resolve and report what would be bundled, downloading nothing";
     readonly group: "other";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: false;
    };
  };
   readonly verify: {
     readonly name: "verify";
     readonly description: "Check every tarball against the registry's checksum";
     readonly group: "other";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: true;
    };
  };
   readonly force: {
     readonly name: "force";
     readonly description: "Replace any existing output file. --no-force refuses instead. Omit to be asked per file";
     readonly group: "output";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: undefined;
    };
  };
   readonly forceSpec: {
     readonly name: "force-spec";
     readonly metavar: "SPEC";
     readonly description: "Always replace these specs, whatever --force says about the rest, e.g. --force-spec tsdown esbuild@0.21.5";
     readonly group: "output";
     readonly type: {
       readonly _tag: "TextList";
    };
  };
   readonly printAbsolutePath: {
     readonly name: "print-absolute-path";
     readonly description: "Report full paths instead of ones relative to the current directory";
     readonly group: "output";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: false;
    };
  };
   readonly json: {
     readonly name: "json";
     readonly description: "Emit a machine-readable summary on stdout";
     readonly group: "output";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: false;
    };
  };
   readonly quiet: {
     readonly name: "quiet";
     readonly alias: "q";
     readonly description: "Only report warnings and errors";
     readonly group: "other";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: false;
    };
  };
   readonly interactive: {
     readonly name: "interactive";
     readonly alias: "i";
     readonly description: "Choose from what the resolution actually found — platforms, layout — instead of guessing flags up front";
     readonly group: "other";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: false;
    };
  };
   readonly yes: {
     readonly name: "yes";
     readonly alias: "y";
     readonly description: "Never prompt; take the default for any question";
     readonly group: "other";
     readonly type: {
       readonly _tag: "Boolean";
       readonly default: false;
    };
  };
}

flagDescriptors

Every flag, in declaration order.

const flagDescriptors: readonly FlagDescriptor[]

FlagGroup

Sections of the surface, in the order --help presents them.

type FlagGroup = "input" | "other" | "output" | "registry" | "selection"

const FlagGroup: {
   readonly Input: "input";
   readonly Output: "output";
   readonly Selection: "selection";
   readonly Registry: "registry";
   readonly Other: "other";
}

flags

const flags: {
   file: Flag<Option<string>>;
   prod: Flag<boolean>;
   lockfile: Flag<string>;
   out: Flag<string>;
   layout: Flag<Option<"dir" | "flat" | "per-spec" | "single">>;
   layoutThreshold: Flag<Option<number>>;
   archiveName: Flag<Option<string>>;
   allVersions: Flag<boolean>;
   maxVersions: Flag<Option<number>>;
   includePrerelease: Flag<boolean>;
   optional: Flag<boolean>;
   peer: Flag<boolean>;
   platform: Flag<readonly string[]>;
   registry: Flag<Option<string>>;
   npmrc: Flag<Option<string>>;
   rewriteTarballHost: Flag<boolean>;
   timeout: Flag<number>;
   retries: Flag<number>;
   concurrency: Flag<number>;
   dryRun: Flag<boolean>;
   verify: Flag<boolean>;
   force: Flag<Option<boolean>>;
   forceSpec: Flag<readonly string[]>;
   printAbsolutePath: Flag<boolean>;
   json: Flag<boolean>;
   quiet: Flag<boolean>;
   interactive: Flag<boolean>;
   yes: Flag<boolean>;
}

ForceMode

What to do about an output file that already exists.

type ForceMode = "all" | "none" | "prompt"

const ForceMode: { readonly Prompt: "prompt"; readonly All: "all"; readonly None: "none"; }

OverwriteAnswer

How one "this file already exists" question was answered.

type OverwriteAnswer = "always" | "keep" | "overwrite"

const OverwriteAnswer: {
   readonly Overwrite: "overwrite";
   readonly Keep: "keep";
   readonly Always: "always";
}

specsArgument

const specsArgument: Argument<readonly unknown[]>

specsDescriptor

const specsDescriptor: {
   readonly name: "spec";
   readonly metavar: "SPEC";
   readonly description: "Packages to bundle: lodash, react@18.2.0, @babel/core@^7, typescript@next";
}

VERSION

Kept in step with package.json; recorded in every bundle manifest.

const VERSION: string

Types

CommandEcho

Everything needed to reconstruct the command that produced this run.

type CommandEcho = {
   readonly specs: readonly string[];
   readonly file?: string | undefined;
   readonly prod?: boolean | undefined;
   readonly lockfile?: string | undefined;
   readonly skipLockfile?: boolean | undefined;
   readonly layout: Layout;
   readonly outDir: string;
   readonly platforms: string;
   readonly optional: boolean;
   readonly peer: boolean;
   readonly allVersions: boolean;
   readonly maxVersions?: number | undefined;
   readonly includePrerelease: boolean;
   readonly verify: boolean;
   readonly force?: ForceMode | undefined;
   readonly forceSpecs?: readonly string[] | undefined;
   readonly registry?: string | undefined;
   readonly archiveName?: string | undefined;
}

Config

The shape the handler receives.

Written out rather than inferred so that the helpers below have a stable, readable contract instead of a conditional type nobody can hover.

type Config = {
   readonly specs: readonly string[];
   readonly file: Option<string>;
   readonly prod: boolean;
   readonly lockfile: string;
   readonly out: string;
   readonly layout: Option<Layout>;
   readonly layoutThreshold: Option<number>;
   readonly archiveName: Option<string>;
   readonly allVersions: boolean;
   readonly maxVersions: Option<number>;
   readonly includePrerelease: boolean;
   readonly optional: boolean;
   readonly peer: boolean;
   readonly platform: readonly string[];
   readonly registry: Option<string>;
   readonly npmrc: Option<string>;
   readonly rewriteTarballHost: boolean;
   readonly timeout: number;
   readonly retries: number;
   readonly concurrency: number;
   readonly dryRun: boolean;
   readonly verify: boolean;
   readonly force: Option<boolean>;
   readonly forceSpec: readonly string[];
   readonly printAbsolutePath: boolean;
   readonly json: boolean;
   readonly quiet: boolean;
   readonly interactive: boolean;
   readonly yes: boolean;
}

FlagDependency

When a flag only means something in the presence of another.

type FlagDependency = { readonly flag: string; readonly value?: string | boolean | undefined; }

FlagDescriptor

type FlagDescriptor = {
   readonly name: string;
   readonly alias?: string | undefined;
   readonly metavar?: string | undefined;
   readonly description: string;
   readonly group: FlagGroup;
   readonly type: FlagType;
   readonly dependsOn?: FlagDependency | undefined;
}

FlagType

What a flag accepts, and what it is when absent.

A default of undefined means the flag is genuinely optional — absent is distinguishable from any value it could carry. That distinction is not decoration: --force relies on it to tell "replace everything" from "refuse" from "ask me per file".

type FlagType = {
   readonly _tag: "Boolean";
   readonly default: boolean | undefined;
} | {
   readonly _tag: "Integer";
   readonly default: number | undefined;
} | {
   readonly _tag: "Text";
   readonly default: string | undefined;
} | {
   readonly _tag: "TextList";
} | {
   readonly _tag: "Choice";
   readonly choices: readonly string[];
} | {
   readonly _tag: "File";
   readonly mustExist: boolean;
} | {
   readonly _tag: "Directory";
   readonly default: string;
}

ForcePolicy

How --force was answered.

type ForcePolicy = { readonly global: ForceMode; readonly specs: readonly string[]; }

LayoutDecision

What to do about the layout, once the resolution is known.

type LayoutDecision = {
   readonly _tag: "Keep";
} | {
   readonly _tag: "Ask";
} | {
   readonly _tag: "Switch";
   readonly layout: Layout;
}

LayoutInput

type LayoutInput = {
   readonly explicitLayout: boolean;
   readonly yes: boolean;
   readonly json: boolean;
   readonly dryRun: boolean;
   readonly interactive: boolean;
   readonly threshold: Option<number>;
   readonly perSpecArchives: number;
   readonly perSpecEntries: number;
   readonly uniquePackages: number;
}

LoadedNpmrc

A resolved .npmrc, and where its values came from.

type LoadedNpmrc = {
   readonly config: NpmrcConfig;
   readonly sources: readonly string[];
   readonly tls: TlsSettings;
   readonly warnings: readonly string[];
}

LockfileCandidate

A lockfile found for a package.json.

type LockfileCandidate = {
   readonly path: string;
   readonly format: LockfileFormat;
   readonly importer: string;
}

LockfilePolicy

What --lockfile was asked for.

type LockfilePolicy = {
   readonly _tag: "Off";
} | {
   readonly _tag: "Detect";
} | {
   readonly _tag: "Manager";
   readonly format: LockfileFormat;
} | {
   readonly _tag: "Path";
   readonly path: string;
}

OverwritePlan

What a run should do about output files that already exist.

type OverwritePlan = {
   readonly force: boolean;
   readonly skipExisting: ReadonlySet<string>;
   readonly overwrite: ReadonlySet<string>;
}

PinnedBy

The lockfile a run was pinned to.

type PinnedBy = {
   readonly path: string;
   readonly format: string;
   readonly lockfileVersion: string;
   readonly importer?: string | undefined;
}

PinnedLockfile

A settled pin: the graph, where it came from, and the roots it names.

type PinnedLockfile = {
   readonly lockfile: LockedTree;
   readonly path: string;
   readonly specs: readonly PackageSpec[];
}

ResolvedInput

What the run will bundle, once --file and --lockfile have been settled.

type ResolvedInput = {
   readonly specs: readonly PackageSpec[];
   readonly lockfile?: LockedTree | undefined;
   readonly lockfilePath?: string | undefined;
   readonly declared?: readonly string[] | undefined;
   readonly detectionRan?: boolean | undefined;
}

TextInput

type TextInput = {
   readonly result: BundleResult;
   readonly options: BundleContext;
   readonly tty: Service;
   readonly elapsedMs: number;
   readonly warnings: readonly string[];
   readonly quiet: boolean;
   readonly absolutePaths?: boolean | undefined;
   readonly echo?: CommandEcho | undefined;
   readonly pinnedBy?: PinnedBy | undefined;
}

On this page