23. Publishing to npm
Code in a codebase is definitions, not files, so there's nothing to point npm publish at. tcm pack makes
the package: the definitions it exports and everything they use, compiled to JavaScript with type declarations,
with entry points that export them by name and a package.json that depends on the packages they import.
23.1 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
Anyone can then use it like any other package:
import { greet, finder, type Greeting } from "greeter";
$ npx greeter Ada hello, Ada
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.
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's, 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 its
package.jsonstarts from: 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, and its dependencies are dropped but for ones shipped in the package (file:./plugin); its scripts and dev dependencies are dropped too suites- command → namespaces of test definitions that run it, relative to the package's:
tcm testruns them as programs (suites). Not packed apps- name → an app the package ships, relative to the package's: each packed as a codebase of its own (below)
libraries- tcm libraries of the codebase the package ships, by package name: each its 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: one others add withtcm lib add, not an npm package. 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 its code speaks: what a library reaching tcm's runtime (the page's globals, for React's routes and data;
#tcm/host, for the server API's tables, jobs and request) is written against. 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 its hash, so a codebase's every version of it is.
stylesheet: a component given a stylesheet file ascss={…}, which the browser is sent as a file it links to, not as text (as the react library's<Stylesheet>).table: a function making a typed table, soconst users = table<User>("users")at the top level is one, its row type's schema kept with it (as 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 (as the server library'severy)
A package's own definitions are its namespace's, except for packages nested in it (their namespaces are theirs), tests, tool configs and the manifest. Its releases are its own too: their versions come from what changed in what it exports, and their notes are its changelog.
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.
23.1.1 Apps
A package can ship apps of the codebase (apps: { share: "share.App" }): tcm pack puts each in it as a codebase of
its own, apps/<name>/codebase.sqlite, holding only the app and what it uses, pinned to the packages it runs on here,
with the app named app. Its recurring jobs (each every(...) beside it, in its namespace) go with it, named as
there (ingest), and so do its plain HTTP routes: the functions in its namespace's http namespace, as http.…
(registry.http.oauth.client as http.oauth.client, which tcm start app --http http=/api serves at
/api/oauth/client), tests left out.
tcm start app there runs it anywhere, building it the first time. It's how tcm ships its
web UI: code that runs from the package finds its apps beside it. The suites tcm test runs, which run the
command from the codebase rather than a package, find them where tcm test packed them (TCM_APPS).
23.1.2 Libraries it ships
A package can ship tcm libraries of the codebase (libraries: ["react"]): tcm pack puts each one's newest release
in it, as one codebase of their own, 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: it's
how tcm ships its React library, tcm lib add tcm react, with no
network. Packing one that ships 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).
23.1.3 Packages that use each other
When one package uses definitions another exports (an app's package using a library's, say), it depends on the
other's oldest release that has those exact definitions (the widest range it works with), 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 it uses something the other exports but no release of it has as it is
(changed since its last release, or not released yet), packing it is refused: release the other first.
What the other keeps to itself (it doesn't export it: a helper, a constant) 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. Two packages of one codebase share code that way without either publishing it: tcm's VS Code extension
groups the Codebase view with the same roles tcm's language server and docs site use (tcmts.cli.pages.ROLES),
which tcm-ts doesn't export.
23.2 Packing definitions directly
Without a manifest, tcm pack takes the definitions on the command line:
tcm pack greet ui=ui --name greeter@1.2.0 --bin greeter=greet.main --files README.md.
23.3 What goes in
In a manifest, or on the command line:
- Entries are namespaces or definitions. A namespace exports every definition beneath it, each under its
last name (
greet.greetasgreet). Several entries can share an entry point, and two definitions that would be 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>instead: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 number it returns becomes the exit code, and anything else is printed. Separate several bins 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. - Dependencies are the packages the code imports, at the versions the codebase has
locked, along with the DefinitelyTyped types tcm added for packages that ship none
(
@types/reactbesidereact). A package that's pinned at two ranges, like code on React 18 next to code on 19, is installed twice side by side under the cache's 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 doesn't import but needs anyway, such as ones it loads at runtime by name, or@types/nodewhen the declarations mention Node's types. Without a version you get the locked one if there is one, and*otherwise.--files a,bcopies files or directories in, such as a README or a licence. They keep their path under the codebase root; files from outside it go in by name.--name <pkg>@<version>sets the version,0.0.0without one;--outdefaults to.tcm/pack/<name>.--descriptionfills that field ofpackage.json. The output directory is replaced each time, so it has to be empty or an earlier pack.
The package is ESM ("type": "module") and needs Node 22.18 or newer, as tcm does.
23.3.1 Code for an editor
Some modules aren't installed but come from where the code runs: vscode, the API VS Code gives its extensions.
Code imports it as usual (import * as vscode from "vscode"); tcm checks it against @types/vscode (provided
like any package), records it unversioned, as it does Node's builtins, and a package never depends
on it. The version of VS Code it needs is the package's to say, as engines.vscode in its packageJson. tcm's
own VS Code extension is packed this way: pnpm tcm pack tcm-ts-vscode.
23.4 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 packs,
but has no app server to talk to: a server function the browser calls, typed tables and background jobs. Use
an app for those (apps, deploying).
tcm itself is packed this way: pnpm verify makes tcm's package from tcm's own codebase, and the test suite
passes against it (developing).