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 a CS… error.
  • TPS#### — the compiler and the build: errors in TPS0001TPS0099, warnings from TPS0100.

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.

© 2026 Curiosity. All rights reserved.