Surfaces and Packages
The most important distinction in Tsonic is:
- surface = ambient world
- package = imported library or source package
Confusing those two is the fastest way to misunderstand the stack.
Surfaces
Tsonic separates the language prelude from the ambient runtime personality.
- compiler core: always-on noLib baseline
clr: default CLR-first ambient surface@tsonic/js: JavaScript ambient surface
A workspace selects one active surface.
Surface examples
clr
Use CLR APIs explicitly:
import { Console } from "@tsonic/dotnet/System.js";
Console.WriteLine("hello");
@tsonic/js
Use JS-style ambient APIs naturally:
console.log(" hello ".trim());
const value = JSON.parse<{ ok: boolean }>('{"ok": true}');
Packages
Packages are imported dependencies. They are not ambient worlds.
Examples:
@tsonic/nodejs@tsonic/express@tsonic/dotnet@tsonic/aspnetcore- local workspace packages
That is why:
@tsonic/jscan be a surface@tsonic/nodejsis still a package
First-party source packages
These are authored in TypeScript and consumed as source packages:
@tsonic/js@tsonic/nodejs@tsonic/express
Each uses tsonic.package.json metadata to declare:
- exported subpaths
- ambient files
- module aliases
- surface compatibility
- runtime metadata where needed
The compiler treats these manifests as the package authority. It does not infer
source-package semantics from naming conventions or from the presence of a
node_modules directory.
Example:
{
"schemaVersion": 1,
"kind": "tsonic-source-package",
"surfaces": ["@tsonic/js"],
"source": {
"exports": {
".": "./src/index.ts",
"./path.js": "./src/path.ts"
},
"moduleAliases": {
"node:path": "./path.js"
}
}
}
When an app imports node:path, package metadata proves that the alias belongs
to @tsonic/nodejs and resolves it to the concrete source file. If that
metadata is missing or incompatible with the active surface, the compiler must
diagnose the import rather than guess.
Example: @tsonic/nodejs
The Node package is not a surface. Instead it is a package that:
- is compatible with
@tsonic/js - declares
node:*aliases - exports subpaths like
./fs.js,./http.js, and./path.js - can pull in framework/runtime requirements through package metadata
That is why the normal model is:
- workspace surface:
@tsonic/js - package dependency:
@tsonic/nodejs
Generated binding packages
These are generated from CLR metadata by tsbindgen:
@tsonic/dotnet@tsonic/aspnetcore@tsonic/microsoft-extensions@tsonic/efcore*
They are:
- packages, not surfaces
- generated, not authored source packages
- owned by
tsbindgen, not by the source-package repos
Generated bindings carry CLR identity metadata. Authored source packages carry source ownership metadata. The compiler keeps those separate even when both describe the same public API shape.
Four package families you should keep separate
- surface packages
- ambient world selection
- example:
@tsonic/js
- authored source packages
- TypeScript source compiled transitively
- examples:
@tsonic/nodejs,@tsonic/express
- generated CLR binding packages
- declaration + metadata packages from
tsbindgen - examples:
@tsonic/dotnet,@tsonic/aspnetcore
- declaration + metadata packages from
- workspace-local package references
- sibling projects referenced as
sourceordll
- sibling projects referenced as
Practical decision table
| You want | Use |
|---|---|
| JS globals and JS-style methods | surface: "@tsonic/js" |
| Node-style modules | @tsonic/nodejs package |
| Express-style routing | @tsonic/express package |
| CLR BCL APIs | @tsonic/dotnet |
| ASP.NET Core APIs | @tsonic/aspnetcore |
| Local workspace package compiled transitively | references.packages with mode: "source" |
| Local workspace package as assembly boundary | references.packages with mode: "dll" |
Source-owned and source-backed values
In compiler reports, source-owned means the package or type is authored by a
Tsonic source package and owned by its tsonic.package.json. Source-backed
means a value, call result, constructor, or alias carries metadata that came
from such a source package even after it flows through another expression.
Example:
import { join } from "node:path";
export function main(): void {
const file = join("src", "App.ts");
console.log(file);
}
Flow:
node:pathis resolved through@tsonic/nodejssource metadata.joinis a source-backed import, not a guessed CLR or JS global.- The call return keeps the package's declared string carrier.
- Later overload, union, and storage checks compare the canonical type identity
for that carrier rather than the text
stringor a package-relative name.