Publishing to npm
A codebase holds definitions, not files, so there's nothing to point npm publish at. tcm pack makes the
package: the exported definitions and everything they use, compiled to JavaScript with type declarations, entry
points that export them by name, and a package.json depending on the packages they import.
Packages
A definition named manifest makes its namespace a package. A codebase can hold any number:
// tcm: namespace greet
import escape from "escape-string-regexp";
/** Greetings, and patterns that find them. */
export const manifest = { name: "greeter", bin: { greeter: "main" } };
export interface Greeting {
to: string;
text: string;
}
/** A greeting, and a pattern that finds it. */
export function greet(to: string): Greeting {
return { to, text: `hello, ${to}` };
}
export const finder = (g: Greeting): RegExp => new RegExp(escape(g.text));
export function main(...names: string[]): number {
for (const n of names) console.log(greet(n).text);
return names.length ? 0 : 2;
}
$ tcm release greeter released greeter@0.1.0 as #pnlegqdu: its first release $ tcm pack greeter@0.1.0 packed greeter@0.1.0 into .tcm/pack/greeter: 4 module(s), depends on escape-string-regexp $ cd .tcm/pack/greeter && npm publish
Then anyone can use it:
import { greet, finder, type Greeting } from "greeter";
$ npx greeter Ada hello, Ada
tcm pack <package>@<version>packs a release: its definitions as they were, at its version, with itsCHANGELOG.md.tcm pack <package>packs it as it is on the branch, at version0.0.0.
The manifest is a definition like any other: hashed, in the history, diffed and reviewed in
contributions. Names in it are relative to its namespace, so tcm mv greet hello takes the
package along. A manifest at the root makes the whole codebase one package.
A package's own definitions are its namespace's, except nested packages (theirs), tests, tool configs and the manifest. Its release versions come from what changed in what it exports, and their notes are its changelog.
name- the npm package's name
exports- subpath (
".","./ui") → names under the namespace whose definitions it exports (""is all of it). Default{ ".": "" } bin- command → the definition it runs
with- packages the code needs but doesn't import, like
@types/node@^24 files- files to copy in: paths relative to the codebase root (a README, a licence), or the codebase's own files relative to the package, written out under their last name: a namespace of them as a directory (
docsisdocs/), one as itself (files.READMEisREADME.md) packageJson- a JSON file of the codebase (relative to the package's namespace) that
package.jsonstarts from, for fields tcm doesn't write, like a VS Code extension'scontributes,publisherandengines. tcm's own fields win (type,exports,bin,version);mainis the main entry if it gives one; its dependencies are dropped except ones shipped in the package (file:./plugin); its scripts and dev dependencies are dropped suites- command → namespaces of test definitions that run it, relative to the package:
tcm testruns them as programs (suites). Not packed apps- name → an app the package ships, relative to the package, packed as a codebase of its own (below)
libraries- tcm libraries of the codebase the package ships, by package name: each one's newest release, packed as a codebase of their own (below)
description- defaults to the first paragraph of the manifest's doc comment
npmfalsefor a tcm library, added withtcm lib addrather than from npm. It isn't packed; a package of the codebase that uses it carries its code instead of depending on it; and its release is refused while it uses definitions outside its namespacehost- the version of tcm's host protocol the code speaks: what a library reaching tcm's runtime is written against (the page's globals, for React's routes and data;
#tcm/host, for the server API's tables, jobs and request). An app using it is built only by a tcm serving that version; this one serves1 roles- name (relative) → its role: what tcm's build treats it as, known by hash so every version of it counts.
stylesheet: a component given a stylesheet file ascss={…}, sent to the browser as a linked file, not text (the react library's<Stylesheet>).table: a function making a typed table, so a top-levelconst users = table<User>("users")is one, its row type's schema kept with it (the server library'stable; code received from elsewhere stays a table if it was one).every: a function making a recurring job, so a definition beside an app whose value it makes runs on its schedule (the server library'severy)
Apps
A package can ship the codebase's apps (apps: { share: "share.App" }). tcm pack puts each in as a codebase of
its own, apps/<name>/codebase.sqlite, holding only the app (named app) and what it uses, pinned to the package
versions it runs on here. With it go:
- its recurring jobs (each
every(...)in its namespace), named as there (ingest); - its plain HTTP routes, the functions in its namespace's
httpnamespace, ashttp.…(registry.http.oauth.clientbecomeshttp.oauth.client, whichtcm start app --http http=/apiserves at/api/oauth/client). Tests are left out.
tcm start app in that directory runs it anywhere, building it the first time. Code running from the package finds
its apps beside it: that's how tcm ships its web UI. Suites, which run the command from the codebase
rather than a package, find them where tcm test packed them (TCM_APPS).
Libraries it ships
A package can ship the codebase's tcm libraries (libraries: ["react"]). tcm pack puts each one's newest release
in one codebase, libraries/codebase.sqlite, each library's namespace named as it was and its release recorded, so
it has the same hash there. A command run from the package reads them as the source tcm: that's how
tcm lib add tcm react gets tcm's React library with no
network. Shipping a library with no release, or one that isn't a tcm library, is refused. Suites find them where
tcm test packed them (TCM_LIBRARIES).
Packages that use each other
When one package uses definitions another exports, it depends on the other's oldest release that has those exact
definitions (the widest range that works) and imports them from it: "geo": "^0.2.0" in its package.json,
import { distance } from "geo" in its modules. It doesn't carry a copy, which would duplicate code and break
instanceof across the two. If no release has them as they are (changed since, or never released), packing is
refused: release the other first.
What the other keeps to itself (not exported) is carried instead, like the package's own code, and so is all of a
tcm library it uses (npm: false), which isn't on npm. That way two
packages of one codebase can share code without either publishing it.
Packing definitions directly
Without a manifest, tcm pack takes definitions on the command line:
tcm pack greet ui=ui --name greeter@1.2.0 --bin greeter=greet.main --files README.md.
What goes in
In a manifest, or on the command line:
- Entries are namespaces or definitions. A namespace exports every definition beneath it under its last name
(
greet.greetasgreet). Several entries can share an entry point. Two definitions exported under one name are refused (give them separate subpaths); a type and a value can share a name. - Subpaths:
subpath=<entry>exports from<package>/<subpath>:tcm pack greet ui=ui --name greetermakesgreeterandgreeter/ui. - Bins:
--bin cmd=<name>makes a command that runs the definition astcm rundoes. A function is called with the command's arguments and awaited; a returned number is the exit code, anything else is printed. Separate several with commas (--bin tcm=cli.run,tcm-ts=cli.run). - Modules: every component the entries and bins need, under its hash (
modules/<hash>.js, with a.d.tsbeside it). Doc comments go into the declarations, so editors show them on hover. A file the code imports (an.svg, a.png) is a module too, exporting its content: text as a string, bytes as aUint8Array. - Dependencies: the packages the code imports, at the codebase's locked versions, plus
the DefinitelyTyped types tcm added for packages without their own (
@types/reactbesidereact). A package pinned at two ranges (code on React 18 beside code on 19) is installed twice under aliases ("npm__react__18": "npm:react@18.3.1"), and each module imports the one it was checked against. --with pkg[@version],...adds packages the code needs but doesn't import: ones loaded at runtime by name, or@types/nodewhen the declarations mention Node's types. Without a version: the locked one, else*.--files a,bcopies in files or directories (a README, a licence). They keep their path under the codebase root; files from outside it go in by name.--name <pkg>@<version>sets the name and version (0.0.0without one).--descriptionfills that field.--outdefaults to.tcm/pack/<name>; it's replaced each time, so it must be empty or an earlier pack.
The package is ESM ("type": "module") and needs Node 22.18 or newer, as tcm does.
Code for an editor
Some modules come from where the code runs rather than being installed: vscode, the API VS Code gives
extensions. Import it as usual (import * as vscode from "vscode"). tcm checks it against @types/vscode
(provided like any package) and records it unversioned, like Node's builtins; a package never depends
on it. Say which VS Code it needs as engines.vscode in the packageJson. tcm's own
VS Code extension is packed this way: pnpm tcm pack tcm-ts-vscode.
What a package can hold
A packed package runs on its own, without tcm. Code that only makes sense under tcm dev or tcm start (server
functions the browser calls, typed tables, background jobs) packs, but has no app server to talk to. Use an app for
those (apps, deploying).
tcm itself is packed this way: pnpm verify packs tcm from its own codebase and runs the test suite against it
(developing).