Compiler CLI
The compiler is a plain command-line program named tps. A normal build never
invokes it directly — the Transpose.Build.Target SDK does that as part of
dotnet build — but the CLI is what you use for
watch mode, for build timings, and when
embedding Transpose in another toolchain.
dotnet tool install --global Transpose.Compiler
export PATH="$PATH:$HOME/.dotnet/tools"
Usage
tps <project.csproj | directory> [options]
Passing a directory works when it contains exactly one .csproj; with more than one
the compiler asks you to name it. With no argument at all it uses the current
directory.
--project / -p is the explicit form of the positional argument, and is what the
SDK passes.
Options
Output
| Option | Description |
|---|---|
-o, --out <file.js> |
Output path for the bundle. Defaults to <project>/bin/<assembly>.js. |
--site-dir <dir> |
Output directory for the assembled site. |
-c, --configuration <name> |
Build configuration (default Debug). Release selects the .min.js resource variants where both exist, and emits full IL rather than a metadata-only assembly. |
--emit-package |
Compile the project as a distributable assembly: emit its .NET DLL with the compiled JavaScript and tps.json resources embedded, for other projects to reference. |
--separate-assemblies |
Consume referenced projects as their built DLLs (extracting their embedded JavaScript) instead of recompiling their sources into this bundle. |
--with-runtime |
Prepend the tps.js runtime and the language shim to the output. |
--build-runtime |
Build the runtime library itself. Used to build Transpose.BCL; auto-selected when a project's tps.json declares outputBy: ClassPath. |
References and symbols
| Option | Description |
|---|---|
-r, --reference <dll> |
An extra assembly to reference, for one that is not resolvable from the NuGet cache. Repeatable. |
-D, --define <SYM> |
Define a preprocessor symbol. Repeatable. |
--assembly-version <v> |
The version stamped into the emitted assembly. |
Transpose defines the TRANSPOSE symbol during transpilation, so #if TRANSPOSE
selects browser-only code.
Incremental builds
| Option | Description |
|---|---|
--incremental / --no-incremental |
Reuse the previous build of this project where its inputs are unchanged. Off by default in the CLI; the SDK opts in. |
--cache-dir <dir> |
Put the incremental cache under <dir> instead of the project's obj/ (implies --incremental). A temp directory is fine — losing the cache only costs a full build. |
A build whose files all hash the same does nothing at all. One whose edits are
confined to method and accessor bodies keeps the cached JavaScript of every type it
did not touch, the reflection metadata, and — in a metadata-only configuration — the
.NET assembly. Anything else (a changed declaration, a new or deleted file, a
different reference or setting) is a full build. The emitted output is
byte-identical either way. The cache lives in obj/tps-cache/.
Watch mode
| Option | Description |
|---|---|
--watch |
Rebuild whenever a source file changes — the root project's and every referenced project's — and serve the assembled site over HTTP on localhost. |
--watch-port <n> |
Port for the watch server and its websocket (default 4300). |
Watch mode requires a site build (a project with a tps.json), so it is
incompatible with --emit-package, --build-runtime and --out.
The emitted assembly
| Option | Description |
|---|---|
--metadata-only-assembly / --no-metadata-only-assembly |
Force the .NET assembly to be metadata only (full metadata including private members, no method bodies) or real IL. |
A Transpose assembly is only ever bound against — it cannot execute, because it
binds to the browser-side stand-in BCL — so metadata-only skips a second full bind of
every method body, which is worth about 18% of a large build. The default is
metadata-only for Debug and real IL for Release. A project can pin it with
<TransposeMetadataOnlyAssembly>. Because the SDK refuses to pack a Debug build, a
metadata-only assembly cannot reach a NuGet feed.
Diagnostics and reporting
| Option | Description |
|---|---|
--max-errors <n> |
Cap how many individual errors are printed. By default there is no cap — every error is reported, ordered by file and line. Pass 0 to restore that explicitly. |
--timing |
Print a per-phase timing and allocation breakdown of the build. |
--timing-json <file> |
Also write that breakdown, plus GC and memory totals, as JSON. |
-q, --quiet |
Suppress warning output. |
-h, --help |
Show the built-in help. |
Diagnostic format
Every error and warning the compiler prints uses the
canonical MSBuild diagnostic format,
Origin : Category Code : Text:
/src/App/Main.cs(17,20): error CS0103: The name 'x' does not exist in the current context
tps : error TPS0002: No .csproj found at '/src/Nope'.
MSBuild scans a tool's output line by line and promotes matching lines to real build
errors, which is what makes a compile error land in the IDE's error list, navigable
to the file and line. Diagnostics with a source location carry an absolute path;
those without one are attributed to the tool (tps).
Everything else the compiler prints — progress, the --timing tables, the error
summary — deliberately does not match that format, so a build never grows errors
nobody wrote.
Diagnostic codes
Roslyn's own CS… errors pass through unchanged. Transpose adds two families of its
own, and both are a shipped contract — a retired code is never reused:
TransposeR####— the translator: a construct it cannot emit. These carry a source location, so they behave like aCS…error.TPS####— the compiler and the build: errors inTPS0001–TPS0099, warnings fromTPS0100.
Translator errors
| Code | Meaning |
|---|---|
TransposeR0001 |
Unsupported feature: the construct has no browser equivalent. P/Invoke ([DllImport] / [LibraryImport]) and inline arrays are reported this way. |
TransposeR0002 |
Not implemented: translation of this construct is not supported yet. |
TransposeR0003 |
Duplicate JavaScript member name: two members of a type would be emitted under the same name, which JavaScript cannot represent — only the last would exist at runtime. Give one a different [Name]. C# overloads cannot trigger this (they are numbered), so it means two members carry the same explicit [Name]. |
Compiler errors
| Code | Meaning |
|---|---|
TPS0001 |
Invalid command line. |
TPS0002 |
Project not found. |
TPS0003 |
The project could not be resolved (its .csproj, sources or references). |
TPS0004 |
Internal compiler error. The exception chain is on the diagnostic line; the stack frames follow separately. |
TPS0005 |
A dependency project's build failed. |
TPS0006 |
Emitting the .NET assembly failed. |
TPS0007 |
--watch requires a site build. |
TPS0008 |
An assembly the project binds against needs a newer compiler. See the minimum compiler version. |
Compiler warnings
| Code | Meaning |
|---|---|
TPS0100 |
A reference could not be found. |
TPS0101 |
The runtime bundle is missing. |
TPS0102 |
The --timing-json file could not be written. |
TPS0103 |
The incremental cache could not be written. The build still succeeds. |
TPS0104 |
The watch-mode server failed to start. |
From the MSBuild SDK
| Code | Meaning |
|---|---|
TPS1001 |
Refusing to pack a Debug build. Pack Release. |
TPS1002 |
The compiler exited non-zero. The full captured output is in obj/<Configuration>/<tfm>/tps.log. |
Why dotnet build looks quiet
Since .NET 9, dotnet build defaults to MSBuild's terminal logger whenever output
is a terminal. It renders only errors, warnings and a per-project summary: every
message and every line a tool writes is dropped, at default verbosity and at
-v:normal, regardless of importance. Only -v:detailed, -v:diagnostic or
-tl:false bring them back.
So compile errors still surface — that is what the canonical format above buys — but the progress lines, the build summary and the timing table cannot be shown from a targets file at all. Two ways to see them:
- Build with
dotnet build -tl:false. - Read
obj/<Configuration>/<tfm>/tps.log, which the SDK writes on every build.
This is not a change from h5: the two SDKs invoke the compiler the same way, and an
h5 project shows its output under -tl:false and none under -tl:true, identically.