This repo demonstrates how to structure and evolve a modern TypeScript CLI and shared module architecture using npm workspaces, tsup, and npm pack — without needing to publish to a registry.
- Created two packages:
cli: a simple CLI that prints a greetingshared: a module exportinggreet(name)
cliimportedgreetfrom@esm_learning/shared- Used
tsupto bundlesharedinto the CLI binary (noExternal) - Packed CLI with
npm pack - Integration test installed the
.tgzand ran the CLI via thebinscript
✅ Good for simple tools, but tightly couples CLI and shared
- Promoted
sharedto a standalone, versioned module - Updated
clito treat@esm_learning/sharedas an external dependency - Introduced
prepare-publishscript to convert local"file:../shared"reference to real semver (^1.0.0) before packing - Created end-to-end integration test that:
- Packs both
sharedandcliinto.tgzfiles - Simulates two consumers:
- One imports and uses
@esm_learning/shared - One installs and runs the CLI via
bin
- One imports and uses
- Packs both
✅ This makes both packages portable, reusable, and testable without publishing to npm
- Introduced a
Plugininterface andPluginLoaderclass inshared - Added
runDefaultPlugin()which loads a built-in plugin viats-nodeor dynamic import - CLI updated to call
runDefaultPlugin()after greeting the user - End-to-end test updated to:
- Create a library consumer that writes its own plugin class
- Dynamically loads and runs that plugin using
PluginLoaderfrom@esm_learning/shared - Handles ESM/CJS compatibility and dynamic runtime paths
✅ Demonstrates extensibility: CLI + shared plugin system + consumer plugins — all tested locally
- Uses
vitestto simulate real consumers - Runs
npm installon the.tgzfiles in fresh temp folders - Verifies both library and CLI behavior
- Proves that local packaging workflows can fully replace registry publishing for testing and distribution
- TypeScript +
tsup - npm workspaces
npm packvitest- CJS CLI with shebang
- ESM/CJS dual export for shared
ISC