What tcm does away with — tcm-ts

What tcm does away with

tcm stores each definition by the hash of its code, and names are labels on hashes. That takes away a lot of usual development work. Each claim says where it stops.

Code

  • No dead code to eliminate. Code runs, and apps are bundled, from what they reach: nothing else is written out. Where it stops: npm packages are still tree-shaken by the bundler; a class ships whole.
  • No circular imports. Definitions that refer to each other are stored together as one module, so there's no import order to get wrong. Where it stops: top-level constants that need each other to exist first still fail, as in plain TypeScript.
  • Renames are free. A rename moves a label: no code is edited, nothing breaks. Where it stops: a function's runtime .name keeps the name it was written with (hashing).
  • No half-finished changes. An update carries through to everything that still typechecks; the rest goes on a todo list, keeping its old version.
  • No formatting churn. Formatting never changes a hash, so a restyle is never a change to diff, merge or review.
  • Docs that don't go stale. Links and examples in doc comments are stored by hash, follow renames and updates, and examples are run. Where it stops: links between pages are kept as written (limitations).

Packages and repositories

  • No packages to share code. Code is shared by name across namespaces; tcm pack is only for publishing to npm. Across codebases, code is shared as a library: another codebase's release, mounted under lib.
  • No monorepo. One codebase holds any number of apps and npm packages, even on different majors of a dependency, and packages depend on each other's releases with no workspace tooling. Where it stops: a package can't be packed while it uses another's unreleased or internal code.
  • No package.json to maintain. Import a package: tcm picks its version from the code around it and installs it and its types. Where it stops: tcm init writes a generated package.json for Node's import map (no dependencies in it), and tcm pack writes one for npm.
  • Two versions of a dependency, side by side. Each definition records the range it was written against, so code on one major runs with code on the next. Where it stops: one app can't mix ranges of a package others are built against; transitive dependencies aren't pinned.
  • Release versions and changelogs nobody writes. The version comes from semver checks of the package's API changes, and the notes list them. Where it stops: types are checked, not behaviour.

Building, testing and linting

  • Nothing to build while you work, and builds made once. Node runs stored code directly. A production build is named by the app's hash, reused, and builds in well under a second. Where it stops: JSX and some imported syntax are compiled as they load; a new version's build bundles the whole app again (unchanged chunks keep their URLs).
  • Each definition tested and linted once. Test results and lint findings are cached by hash: only what a change reaches runs again, and end-to-end suites by the code they ran. Where it stops: results that depend on the clock, the network, files or the environment can go stale (limitations).
  • Shared outputs with no setup. Test results and lint findings travel with tcm push and tcm pull, so CI or another machine doesn't redo what passed elsewhere. Where it stops: production builds, watch values and kept constants are each machine's own; a pulled result is trusted as the remote's code is.

Apps

  • No deciding where code runs. Server or browser is inferred from what each definition uses, and browser code calls server functions by importing them: no routes, no fetch calls, no "use client" or "use server". Where it stops: a package export that can't be followed (a computed import()) falls back to a guess for the whole package; calls across must be to async functions, and only plain data crosses.
  • Skew protection with no setup. Calls go by hash and earlier builds stay served, so an open tab keeps calling the code it loaded, and a job runs the code that queued it. Where it stops: a tab on a pruned build, or one reading rows newer code wrote, reloads (410); each server behind a load balancer keeps its own builds.
  • Instant rollbacks. Rolling back moves a name back to an earlier build that's still there. Where it stops: a pruned build is built again first (under a second).
  • Deploys that re-download only what changed. Browser chunks are named by content and kept across deploys; a changed chunk arrives as a diff from the one the browser has. Where it stops: browsers without compression dictionaries get the whole chunk.
  • Easy migrations. A table's row type is its schema: changing it adds a typed step, applied as rows are read, and tcm update says up front what stored rows need. tcm migrate moves a codebase to a new hash format, history and all. Where it stops: keep a step while an older build still writes the old shape.

Built from c482a14e