Complete guide for developing with this TypeScript monorepo.
- Getting Started
- Package Types
- Managing Dependencies
- Development Commands
- How It Works
- Releasing
- GitHub Actions Setup
- Project Structure
When you clone this repository, you'll find an empty monorepo ready for your packages.
pnpm installpnpm newThis will prompt you for:
- Package type:
lib(library),test(test package), orexample(example app) - Package name: e.g.,
my-package - Private: Whether the package should be private (for libs only)
The generator will:
- Create the package structure
- Generate TypeScript configs
- Update workspace path mappings
- Install dependencies automatically
To add additional entry points to a public lib (e.g., @restatedev/my-lib/utils):
pnpm add-entryThis automatically:
- ✅ Creates the source file with placeholder code
- ✅ Updates
package.jsonexports andtypesVersions(for Node 10 compatibility) - ✅ Updates
tsdown.config.tsentry array - ✅ Updates root
tsconfig.jsonpaths for IDE support - ✅ Creates
api-extractor.{entry}.jsonconfig - ✅ Updates
_check:apiscript to validate the new entry - ✅ Formats all modified files
# Watch lib packages (type checking only, no build)
pnpm dev
# Run examples in dev mode (uses source, no build required)
pnpm examples:dev
# Run tests in watch mode (tests source directly)
pnpm test:watchPublic libs are built with tsdown and published to npm:
{
"name": "@restatedev/package-name",
"scripts": {
"build": "tsdown",
"dev": "tsc --noEmit --watch"
},
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"publishConfig": {
"access": "public"
}
}Private libs are source-only (no build step):
{
"private": true,
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts"
}
}Private packages get bundled into public packages automatically! This is useful for internal utilities that don't need to be published separately.
Important: Cross-Package Dependencies
If a publishable package depends on another publishable package in the monorepo, you must add it to the external array in tsdown.config.ts to prevent bundling:
// packages/libs/my-package/tsdown.config.ts
export default defineConfig({
entry: ["src/index.ts"],
// Add any publishable packages from this monorepo that this package depends on
external: ["@restatedev/other-package"],
});This ensures the dependency is treated as an external package (peer dependency) rather than being bundled into your build output.
Adding custom entry points to public libs:
Use pnpm add-entry to add subpath exports like @restatedev/my-lib/utils:
pnpm add-entry
# Select package → Enter entry name (e.g., "utils" or "internal/helpers")This automatically:
- ✅ Creates the source file with placeholder code
- ✅ Updates
package.jsonexports andtypesVersions(for Node 10 compatibility) - ✅ Updates
tsdown.config.tsentry array - ✅ Updates root
tsconfig.jsonpaths for IDE support - ✅ Creates
api-extractor.{entry}.jsonconfig - ✅ Updates
_check:apiscript to validate the new entry - ✅ Formats all modified files
You can then import it separately:
import { foo } from '@restatedev/my-lib'; // Main entry
import { bar } from '@restatedev/my-lib/utils'; // Custom entryAll validation tools pass automatically:
- ✅
check:exports- ATTW validates all subpath exports (including Node 10) - ✅
check:api- API Extractor validates the custom entry - ✅
check:types- TypeScript checks all source files
Test packages use Vitest to test your libraries:
pnpm test # Run all tests (builds dependencies first)
pnpm test:watch # Watch mode - tests source directly (no build needed!)Important: The test script runs tests against built output (production-like), while test:watch automatically resolves library imports from source files for instant feedback during development.
Example packages demonstrate your libraries in action:
pnpm examples:dev # Run all examples (uses source)
pnpm examples:dev demo # Run specific example
pnpm examples:start # Run with built libs (production-like)To add a dependency between packages (e.g., example depends on your lib):
cd packages/examples/my-example
pnpm add "@restatedev/my-lib@workspace:*"The pnpm new and pnpm delete commands automatically run pnpm install after modifying packages to ensure everything is linked properly.
For regular npm packages:
# Add to specific package
pnpm --filter @restatedev/package-name add zod
# Add to root (dev dependencies)
pnpm add -Dw prettierCatalogs help manage shared dependencies (especially peer dependencies) across all packages. This ensures version consistency.
1. Add to catalog in pnpm-workspace.yaml:
catalog:
zod: ^4.1.12
react: ^18.3.12. Use in packages with catalog::
{
"peerDependencies": {
"zod": "catalog:"
},
"devDependencies": {
"zod": "catalog:"
}
}3. Install:
pnpm installThis is perfect for managing peer dependencies consistently across all your packages!
# Package management
pnpm new # Create a new package
pnpm delete # Delete a package
pnpm add-entry # Add custom entry point to a public lib
# Development
pnpm dev # Watch libs (type checking)
pnpm examples:dev # Run all examples (dev mode)
pnpm examples:dev demo # Run specific example
# Building
pnpm build # Build lib packages only
pnpm build:all # Build all packages (libs + examples)
# Testing
pnpm test # Test all packages (built output)
pnpm test:watch # Test in watch mode (source)
# Quality checks
pnpm lint # Lint all packages
pnpm format # Format all files
pnpm check:format # Check formatting
pnpm check:types # Type check all packages
pnpm check:exports # Verify package exports (ATTW)
pnpm check:api # Check for forgotten type exports
pnpm verify # Run all checks (same as CI)
# Utilities
pnpm clean # Clean build artifacts
pnpm clean:cache # Clear turbo caches
pnpm deps:check # Check for outdated dependencies
pnpm deps:update # Update all dependenciesAll commands work from within a package directory too! Just cd into any package and run:
cd packages/libs/my-package
pnpm build # Builds this package AND its dependencies
pnpm test # Tests this package (builds dependencies first)
pnpm check:types # Type checks this package (builds dependencies first)
pnpm dev # Dev mode (type checking only)
pnpm lint # Lint this packageThis works because package scripts use turbo run --filter={.}... which:
- Runs the command for this package
- Automatically builds upstream dependencies first
- Leverages Turbo's caching for speed
Tip: Always run pnpm verify from the root before committing - it runs all the checks that CI will run!
In dev mode, your examples and tests can use lib packages without building them:
- Libs:
tsc --noEmit --watchfor type checking only - Examples: Use TypeScript path mappings to source files
- Tests: Vitest automatically resolves libs from source in watch mode
This means instant feedback - change lib code and see results immediately!
Dev workflow:
pnpm dev # Type check libs
pnpm examples:dev # Run examples with source
pnpm test:watch # Test source directlyWhen you run pnpm build (or build:all), pnpm test, or pnpm examples:start:
- Libs are built to
dist/with tsdown (ESM + CJS + TypeScript declarations) - Examples and tests use the built output
- This validates your actual published code
Build commands:
pnpm build- Builds lib packages only (faster, default)pnpm build:all- Builds everything including examples (used in CI)
The repo uses a layered TypeScript configuration:
Root Level:
tsconfig.base.json- Shared compiler optionstsconfig.json- Extends base + adds path mappings for IDE
Package Level:
tsconfig.json- Extends root (inherits path mappings)tsconfig.build.json- Extends base (clean builds)
Path mappings are auto-generated when you create/delete packages via pnpm generate:configs.
Turbo runs automatically when you use pnpm commands:
- Smart caching - Skip unchanged work
- Parallel execution - Run tasks simultaneously
- Dependency awareness - Build deps first automatically
You don't need to think about Turbo - just use pnpm build, pnpm test, etc!
How it works:
- Package scripts use
turbo run _build --filter={.}... - The
--filter={.}...means "this package and its dependencies" - Internal
_build,_test, etc. tasks havedependsOn: ["^_build"]inturbo.json - Turbo automatically builds dependencies before running the task
This means you can run pnpm build from any package directory and it will automatically build dependencies first!
This monorepo supports two release workflows:
Note: Only public (publishable) packages will appear in the changeset prompt. Private packages are automatically excluded from version bumps and publishing.
This workflow uses Changesets for automated version management.
pnpm changesetSelect packages to version and describe changes.
git add .changeset
git commit -m "Add changeset for new feature"When merged to main:
- GitHub Actions detects the new version in package.json
- Automatically creates a git tag (e.g.,
v1.2.3) - Creates a GitHub release
- Publishes packages to npm
Use this workflow for hotfix branches that need to be released without merging to main first.
git checkout -b hotfix/critical-fixpnpm changeset # Create changeset for the fix
pnpm version # Apply changesets to update package.json and CHANGELOGgit add .
git commit -m "Release v1.2.3 - critical fix"
git push origin hotfix/critical-fixgit tag v1.2.3
git push origin v1.2.3Manually create a GitHub release for the tag:
gh release create v1.2.3 --title "Release v1.2.3" --generate-notesOr via GitHub UI: Go to Releases → Draft a new release → Select the tag → Publish release
When the release is published, GitHub Actions automatically publishes packages to npm.
Important: Never run pnpm release locally. Always let GitHub Actions handle publishing to npm to ensure consistency and proper CI checks.
The repo includes three workflows:
Runs on every PR:
- Format check
- Lint
- Type check
- Build all packages (
pnpm build:all) - Test
- Verify exports (ATTW)
- Verify API (API Extractor)
Runs automatically on push to main:
- Checks for new package versions in
packages/libs/*/package.json - If new version detected (version doesn't have a git tag):
- Creates and pushes git tag (e.g.,
v1.2.3) - Creates GitHub release with auto-generated notes
- Calls the publish workflow to publish to npm
- Creates and pushes git tag (e.g.,
This workflow enables the automatic changesets workflow: when you merge a PR that bumps the version in package.json (via changesets), this workflow automatically creates the tag and release.
Runs when:
- A GitHub release is manually published
- Manually triggered via workflow_dispatch
This workflow enables the manual hotfix workflow: you can create a hotfix branch, update package.json version (ideally using changesets), commit the change, create a git tag, then manually publish a GitHub release for that tag to trigger publishing.
Both release workflows use this shared workflow that:
- Installs dependencies
- Builds lib packages (
pnpm build) - Publishes public packages to npm
To enable automatic publishing to npm:
-
Create NPM token:
- Go to https://www.npmjs.com/settings/YOUR_USERNAME/tokens
- Create an "Automation" token
- Copy the token
-
Add to GitHub Secrets:
- Go to your repo Settings → Secrets and variables → Actions
- Click "New repository secret"
- Name:
NPM_TOKEN - Value: Your NPM token
-
Verify package configuration:
Public packages should have:
{ "publishConfig": { "access": "public" } }The generator automatically adds this when you create a public lib.
.
├── packages/
│ ├── libs/ # Library packages (empty to start)
│ ├── tests/ # Test packages (empty to start)
│ └── examples/ # Example apps (empty to start)
├── .github/
│ └── workflows/ # CI/CD workflows
├── .templates/ # Plop templates
├── scripts/ # Utility scripts
├── .changeset/ # Changesets configuration
├── turbo.json # Turbo task configuration
├── plopfile.js # Package generator
└── pnpm-workspace.yaml # Workspace config with catalogs
ATTW (Are The Types Wrong) automatically tests your packages against different module resolution modes (node16, bundler, etc.). This catches issues like:
- Missing type exports
- Incorrect module formats
- Path resolution problems
API Extractor will error if you use types in public APIs that aren't exported. This ensures your package consumers can use all necessary types.
Mark packages as private in package.json to prevent accidental publishing. Private lib packages don't need a build script - they're bundled into public packages automatically through tsdown.