packall

API reference

Use the engine, or the command itself, from your own code.

Everything the CLI does is available as a library. Use it when bundling is a step inside something else — a release pipeline, an internal tool, a service that prepares offline artifacts on request.

The packages are built on Effect: the engine reaches the outside world only through services, which is what lets the same code run in Node and in a browser tab.

Which package

Package
@packall/coreThe engine. Resolution, download, verification, layout, archiving. Knows nothing about npm.
@packall/registry-npmThe npm-protocol backend, plus .npmrc parsing and credentials.
@packall/cliThe command: the parser, the flag surface as data, the progress renderer and the summary formatters.

Each has a /node entry point holding the parts that need a real machine — a tar writer, a socket, a terminal, an .npmrc on disk. Importing the main entry point pulls in no node: module, which is what keeps it usable in a browser.

npm i @packall/core @packall/registry-npm

Bundle something

bundle resolves, downloads, verifies and packages. It needs a registry backend, a progress sink, an archiver and Node's filesystem services.

import { NodeHttpClient, NodeServices } from "@effect/platform-node";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";

import { bundle, defaultBundleOptions, layerSilentProgress, parseSpec } from "@packall/core";
import { layerArchiver } from "@packall/core/node";
import { layer as layerRegistry } from "@packall/registry-npm";

const program = bundle([parseSpec("ms@2.1.3")], {
	...defaultBundleOptions,
	outDir: "./bundles",
	toolVersion: "1.0.0",
});

const result = await program.pipe(
	Effect.provide(
		Layer.mergeAll(
			layerRegistry().pipe(Layer.provide(NodeHttpClient.layerUndici)),
			layerSilentProgress,
			layerArchiver,
			NodeServices.layer,
		),
	),
	Effect.runPromise,
);

console.log(result.artifacts);
// [{ kind: "archive", path: "bundles/ms@2.1.3.tgz", bytes: 4486, packageCount: 1, … }]

Signatures and options are in the @packall/core referencebundle, BundleOptions, BundleResult.

Resolve without downloading

plan stops after resolution, so you can inspect what a run would pull before committing to it. It needs only a registry and a progress sink — no filesystem at all.

import {
	defaultBundleOptions,
	layerSilentProgress,
	parseSpec,
	plan,
	summarize,
} from "@packall/core";

const resolution = await plan([parseSpec("react-dom@19.2.8")], {
	...defaultBundleOptions,
	outDir: ".",
}).pipe(
	Effect.provide(
		Layer.mergeAll(
			layerRegistry().pipe(Layer.provide(NodeHttpClient.layerUndici)),
			layerSilentProgress,
		),
	),
	Effect.runPromise,
);

resolution.packages.map((pkg) => `${pkg.name}@${pkg.version}`);
// ["react@19.2.8", "react-dom@19.2.8", "scheduler@0.27.0"]

summarize(resolution);
// { uniquePackages: 3, perSpecArchives: 1, perSpecEntries: 3 }

Pass the resolution to bundleResolved to carry on.

Follow the progress

layerSilentProgress discards events. To report them, take a callback:

import { layerCallbackProgress } from "@packall/core";

const progress = layerCallbackProgress((event) => {
	console.log(event._tag, event);
});

Every event is in ProgressEvent. The CLI's own renderer — spinner, bar, byte totals, ETA — is ProgressRenderer and is platform-neutral: it takes write, columns and a ticker as options, so it can drive a terminal, a log file or a browser.

Run the command itself

If you want the CLI's behaviour rather than the engine's — the same parser, flags, prompts, progress and summary — run the command with an argv array:

import { NodeServices } from "@effect/platform-node";
import { Command } from "effect/unstable/cli";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";

import { bundleCommand } from "@packall/cli";
import { layerNpmrc, layerTransport, layerTty } from "@packall/cli/node";
import { layerArchiver } from "@packall/core/node";

const platform = Layer.mergeAll(NodeServices.layer, layerArchiver);
const run = Command.runWith(bundleCommand, { version: "1.0.0" });

await run(["ms@2.1.3", "--dry-run"]).pipe(
	Effect.provide(
		Layer.mergeAll(
			platform,
			layerNpmrc,
			layerTransport,
			layerTty.pipe(Layer.provide(platform)),
		),
	),
	Effect.runPromise,
);

Swapping those Node layers for browser ones is the entire difference between packall in a terminal and packall in a tab.

Present the command surface

descriptors is the flag surface as data — name, alias, metavar, description, default, group, and which flags depend on which. The parser is built from it, so anything that reads it cannot describe a different tool. The options reference and the runner's controls are both generated from it.

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

descriptors.layout;
// { name: "layout", description: "per-spec: …", group: "output",
//   type: { _tag: "Choice", choices: ["per-spec", "single", "dir"] } }

A different registry

Registry is a service with three operations — preflight, packument, download. Supporting a registry that does not speak the npm protocol means implementing that interface and nothing else; everything downstream is written against it.

See Registry and Registry.Service.

On this page