Adding a Package
When a new shared package is worth creating, and the four small steps to make one that every app can import.
A package is a folder under packages/. It holds code that more than one app
imports. It is never published to npm. It stays inside zap.ts.
Two small packages make good models: packages/storage and packages/queues.
Open them while you read this page.
Step 1: make the folder
Pick a short, plain name. Then create two folders:
mkdir -p packages/invoicing/src
Put your code in src. Everything else the package needs is two small files.
Step 2: write the package.json
Create packages/invoicing/package.json:
{
"name": "@zap-ts/invoicing",
"private": true,
"type": "module",
"sideEffects": false,
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@zap-ts/environment": "workspace:*",
"effect": "catalog:"
},
"devDependencies": {
"@zap-ts/tsconfig": "workspace:*",
"typescript": "catalog:"
}
}
Here is what each line is for.
namealways starts with@zap-ts/. That prefix is how the apps import it.privatestops anyone publishing the package to npm by accident.type: "module"means the package usesimport, notrequire.sideEffects: falsetells the bundler it can drop code nobody imports.scripts.typecheckis the script the rootpnpm run typecheckcalls.
There is no version field, and no version number anywhere. See
Dependencies for the rule.
The exports map
exports lists the paths other code may import. Nothing else in the folder can
be reached from outside.
The example above has one entry, ".". So an app writes:
import { createInvoice } from "@zap-ts/invoicing";
You can add more entries when the package grows. packages/database does this:
{
"exports": {
".": "./src/client.ts",
"./schema": "./src/schema.ts"
}
}
That second entry becomes @zap-ts/database/schema.
Keep the map small. Each entry is a promise to the rest of the codebase, and a file you cannot move freely afterwards.
Step 3: write the tsconfig
Create packages/invoicing/tsconfig.json. It is two lines:
{
"extends": "@zap-ts/tsconfig/base.json",
"include": ["src"]
}
extends pulls in the shared settings from tooling/tsconfig. Every package
uses the same rules, so a type error looks the same everywhere.
There are four shared files. Pick the one that matches where your code runs.
| File | Use it when |
|---|---|
base.json |
Plain TypeScript, no browser and no Node types |
node.json |
The code uses Node, for example to read a file |
react.json |
The package holds React components |
webworker.json |
The code runs on Cloudflare Workers |
Most packages extend base.json, like storage and queues do.
Step 4: use it from an app
In the package.json of the app that needs your package, add it with
workspace:*:
{
"dependencies": {
"@zap-ts/invoicing": "workspace:*"
}
}
workspace:* means “the copy in this repository”. pnpm links the folder
straight into the app. Nothing is downloaded, and nothing is published.
Step 5: install
pnpm install
pnpm reads every package.json again, sees the new folder, and links it.
You do not have to list the new package anywhere else. pnpm finds it because it
sits under packages/.
Now check that everything still types:
pnpm run typecheck
A quick review
mkdir -p packages/<name>/src- Write
package.jsonwith the@zap-ts/name and anexportsmap. - Write
tsconfig.jsonthat extends the shared one. - Add
"@zap-ts/<name>": "workspace:*"to the app that needs it. - Run
pnpm install.
That is the whole package. Two config files and your code.
Before you add the second one
Ask the question from the top of this page again. Does a second app really need this code?
If the answer is no, the code belongs in the app. You can always move it later, and moving it later costs less than maintaining a folder nobody needed.
Related
- Dependencies —
catalog:andworkspace:* - Project structure — what lives where
- Storage — a small package to copy
- Queues — an even smaller one
- Testing — where the tests of your package go