Edit source

Projects and configuration

A Tsonic project is an npm package containing package.json, tsonic.json, and TypeScript source files.

Create the complete default layout rather than assembling these files by hand:

npm create tsonic@latest my-app -- --target csharp

Use --target rust for Rust. The rest of this page explains the files that the creator produced and the controls used by advanced projects.

my-app/
├── package.json
├── tsonic.json
└── src/
    ├── App.ts
    └── model.ts

package.json has two jobs:

  1. it declares ordinary npm dependencies and workspaces;
  2. it makes installed Tsonic targets and capabilities discoverable.

Use ESM:

{
  "name": "my-app",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "tsonic build --project tsonic.json"
  },
  "devDependencies": {
    "@tsonic/cli": "^0.1.0",
    "@tsonic/target-csharp": "^0.1.0"
  }
}

Local TypeScript imports use ESM output spelling:

import { User } from "./model.js";

The authored file remains model.ts. TSTS resolves the .js specifier using Node ESM rules.

Source roots

{
  "entryPoint": "App.ts",
  "rootFiles": ["App.ts", "worker.ts"],
  "rootDir": "src",
  "outDir": "out",
  "targets": [{ "id": "csharp" }]
}
  • rootDir is resolved from the directory containing tsonic.json.
  • entryPoint and every rootFiles entry are resolved below rootDir.
  • rootFiles defaults to the entrypoint and must include it when supplied.
  • Imported dependencies are followed through the checked ESM graph.
  • outDir is resolved from the project directory, not from rootDir.
  • Target tools keep disposable state under cacheDir, which defaults to .tsonic/cache beside tsonic.json. Set it when that location is not writable. It cannot overlap outDir.

Tsonic does not read a tsconfig.json. Fields such as compilerOptions, paths, baseUrl, extends, and TypeScript project references are rejected instead of being silently ignored.

Four kinds of control

Keep each choice with its owner:

ControlExampleOwner
Source semanticsexact int32, JS surface, Node importTsonic source and selected target
Target semanticsC# nullable mode, Rust foundationtarget options in tsonic.json
Generated native projectC# RuntimeIdentifiersupported target project options
Open native configurationWeb SDK, Cargo target, linker scriptuser-owned native project or native CLI

There is no generic override bag. The host accepts only compiler-owned project fields. Each target validates its own options object.

For example, C# owns targetFramework, outputType, and publishAot. Other scalar MSBuild properties may be supplied through C# properties. Rust does not expose arbitrary Cargo TOML through tsonic.json; use a user-owned Cargo.toml when the generated manifest is not enough.

Generated native projects

Without projectFile, the target emits a complete native project:

out/
├── csharp/
│   ├── TsonicGenerated.csproj
│   ├── src/
│   └── generated/
└── rust/
    ├── Cargo.toml
    └── src/

The complete outDir is compiler-owned. Do not place authored files there. Tsonic builds into staging and replaces the published tree only after every selected target succeeds. A failed build leaves the previous successful output intact.

User-owned native projects

Set a target projectFile when the native project must control its SDK, dependencies, platform, linker, profiles, or deployment:

{
  "targets": [{
    "id": "csharp",
    "options": {
      "projectFile": "native/Example.csproj",
      "outputType": "Exe"
    }
  }]
}
{
  "targets": [{
    "id": "rust",
    "options": {
      "projectFile": "native/Cargo.toml",
      "foundation": "std",
      "outputType": "bin"
    }
  }]
}

Tsonic still uses target options to decide source semantics and generated source shape. It emits no native project file and never edits the project you named. The native project must include the generated sources and declare every runtime or third-party dependency they use.

See the complete C# project guide and Rust project guide.

Multiple targets

One project may select more than one target:

{
  "entryPoint": "index.ts",
  "rootDir": "src",
  "targets": [
    { "id": "csharp", "surfaces": ["js"] },
    { "id": "rust", "surfaces": ["js"] }
  ]
}

This works when the source contract and entry behavior are valid for every selected target. C# and Rust applications have different entrypoint rules, so portable applications normally share library packages and use a small target-specific entry module. See applications and libraries.