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/core | The engine. Resolution, download, verification, layout, archiving. Knows nothing about npm. |
@packall/registry-npm | The npm-protocol backend, plus .npmrc parsing and credentials. |
@packall/cli | The 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-npmBundle 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 reference —
bundle, 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.