Build Output
Understanding what Tsonic generates during compilation.
Build Pipeline
TypeScript → IR → C# → .NET → Native Binary
│ │ │ │ │
Parse Build Emit Publish Copy
- Parse: TypeScript Compiler API reads source files
- Build IR: Create intermediate representation
- Emit C#: Generate C# code from IR
- Publish: Run
dotnet publish(NativeAOT by default for executables) - Copy: Copy binary to
out/directory
Directory Structure
After building:
workspace/
├── tsonic.workspace.json
├── libs/
└── packages/
└── my-app/
├── tsonic.json
├── src/
│ └── App.ts # Your source
├── generated/ # Generated C# code
│ ├── src/
│ │ └── App.cs # C# from your TypeScript
│ ├── Program.cs # Entry point wrapper
│ ├── tsonic.csproj # .NET project file
│ ├── bin/ # .NET build output
│ └── obj/ # .NET intermediate files
└── out/
└── my-app # Final executable
Generated Files
C# Source Files
Each .ts file becomes a .cs file:
src/App.ts → generated/src/App.cs
src/utils/Math.ts → generated/src/utils/Math.cs
src/models/User.ts → generated/src/models/User.cs
__tsonic_json.g.cs (Conditional)
Generated only when your code uses JsonSerializer. Contains NativeAOT-compatible
JSON serialization context:
// __tsonic_json.g.cs
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(List<User>))]
internal partial class __TsonicJsonContext : JsonSerializerContext { }
internal static class TsonicJson
{
internal static readonly JsonSerializerOptions Options = new()
{
TypeInfoResolver = __TsonicJsonContext.Default
};
}
This file is automatically generated based on types used with JsonSerializer.Serialize()
and JsonSerializer.Deserialize<T>() calls in your code.
Program.cs
Entry point wrapper that calls your main():
// Program.cs
public static class Program
{
public static void Main(string[] args)
{
global::MyApp.App.main();
}
}
For async main:
public static class Program
{
public static async Task Main(string[] args)
{
await global::MyApp.App.main();
}
}
tsonic.csproj
.NET project file with NativeAOT configuration:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>MyApp</RootNamespace>
<AssemblyName>app</AssemblyName>
<PublishAot>true</PublishAot>
<PublishSingleFile>true</PublishSingleFile>
<PublishTrimmed>true</PublishTrimmed>
<StripSymbols>true</StripSymbols>
</PropertyGroup>
</Project>
Output Types
Executable (Default)
Single native binary:
packages/<project>/out/<name> (Linux/macOS)
packages/<project>/out/<name>.exe (Windows)
Configuration:
{
"output": {
"type": "executable"
}
}
Library
Managed (.NET) library
Standard managed library build:
dist/
├── net10.0/
│ ├── MyLib.dll
│ ├── MyLib.xml # Documentation
│ └── MyLib.pdb # Symbols
└── net8.0/
└── ... # Multi-target
Library builds also emit shippable CLR bindings under:
dist/tsonic/bindings/
NativeAOT (native library)
When output.nativeAot: true for a library, Tsonic runs dotnet publish -r <rid> and
copies the full publish output into dist/:
dist/
└── net10.0/
├── MyLib.dll # Managed DLL (copied for reflection/bindings)
└── linux-x64/
└── publish/
├── libMyLib.so # Native shared library (Linux)
├── MyLib.pdb # Optional symbols (if emitted)
└── ... # Runtime/native deps as produced by dotnet publish
Notes:
- The native output is RID-specific, so it is placed under
dist/<tfm>/<rid>/publish/. - Use
output.nativeLibto pick"shared"(default) vs"static".
Configuration:
{
"entryPoint": "src/index.ts",
"output": {
"type": "library",
"targetFrameworks": ["net10.0", "net8.0"],
"nativeAot": false
}
}
NativeAOT library example:
{
"output": {
"type": "library",
"targetFrameworks": ["net10.0"],
"nativeAot": true,
"nativeLib": "shared"
}
}
Build Options
NativeAOT Settings
These defaults apply to output.type: "executable" unless specified in tsonic.json.
| Option | Default | Description |
|---|---|---|
nativeAot |
true |
Enable NativeAOT publish for executables |
singleFile |
true |
Single executable (NativeAOT executables only) |
trimmed |
true |
Remove unused code (NativeAOT executables) |
stripSymbols |
true |
Remove debug info |
selfContained |
true |
Include runtime |
For output.type: "library", only these keys apply:
| Option | Default | Description |
|---|---|---|
nativeAot |
false |
Enable NativeAOT publish for libraries |
nativeLib |
shared |
Native library kind (shared or static) |
Optimization
{
"optimize": "speed" // or "size"
}
| Mode | Binary Size | Performance |
|---|---|---|
speed |
Larger | Faster |
size |
Smaller | Slightly slower |
Binary Size
Typical sizes for "Hello World":
| Platform | Size |
|---|---|
| Linux x64 | ~3 MB |
| macOS ARM64 | ~3 MB |
| Windows x64 | ~3.5 MB |
Size increases with:
- More code
- More .NET dependencies
- Disabled trimming
- Debug symbols
Keeping Build Artifacts
For debugging, keep generated files:
tsonic build --keep-temp
Or inspect manually:
tsonic generate # Generate C# only
cd packages/<project>/generated
dotnet build # Build manually
Cross-Compilation
Build for different platforms:
# Linux x64
tsonic build src/App.ts --rid linux-x64
# macOS ARM64
tsonic build src/App.ts --rid osx-arm64
# Windows x64
tsonic build src/App.ts --rid win-x64
Cross-compilation requires .NET SDK with target runtime.
Gitignore
Recommended .gitignore:
# .NET build artifacts (per-project)
packages/*/generated/bin/
packages/*/generated/obj/
# Output executables
packages/*/out/
# .NET artifacts
bin/
obj/
# Dependencies
node_modules/
# Internal tooling artifacts (restore scratch, caches)
.tsonic/
Troubleshooting Builds
Check Generated C#
tsonic generate src/App.ts
cat generated/src/App.cs
Build Manually
cd generated
dotnet build
dotnet run
Verbose Output
tsonic build src/App.ts --verbose
Shows:
- Files being processed
- Generated file paths
- dotnet command output