Migrating from h5

Transpose is the next generation of the h5 C#-to-JavaScript compiler. The compiler was rebuilt around a clean-room Roslyn translator (the legacy Bridge/NRefactory pipeline is gone), and the project was rebranded from H5 to Transpose.

Migrating an existing h5 project is mostly a mechanical rename of packages, the config file, and namespaces. The code-generation attributes, the Script.Write interop surface, and your component code behave the same.

This guide covers everything that changes. It is based on the real migration of the Tesserae UI toolkit and its sample app.

What changed at a glance

Concept h5 Transpose
Runtime global (generated JS) H5 Transpose
Language-helper shim H5R TransposeR
Runtime bundle / config / module h5.js / h5.json / h5 tps.js / tps.json / tps
Compiler command h5 (the h5-compiler global tool) tps (invoked by the SDK)
Codegen attribute namespace H5 Transpose
Web-API bindings namespace H5.Core Transpose.Core
Conditional-compilation symbol H5 TRANSPOSE

Package and SDK mapping

h5 package Transpose package Notes
h5.Target (SDK) Transpose.Build.Target The MSBuild SDK that runs tps per project
h5 (base library) Transpose.BCL The package id is Transpose.BCL; the assembly/DLL stays Transpose.dll
h5.Core Transpose.Core DOM / ES5 / ES6 bindings
h5.Newtonsoft.Json Transpose.Newtonsoft.Json
h5.HttpClient Transpose.HttpClient
h5.Howler Transpose.Howler Not yet published to NuGet
h5.p2 Transpose.P2 Not yet published to NuGet
h5.WebGL2 Transpose.WebGL2 Not yet published to NuGet
h5.template Transpose.Template The dotnet new template; not yet published to NuGet
h5-compiler (global tool) Transpose.Compiler No longer needed for a build — see Step 4
Four of these are not on NuGet yet

Transpose.Howler, Transpose.P2, Transpose.WebGL2 and Transpose.Template exist in the repository and have release pipelines, but have not been published. If your h5 project depends on one of the binding libraries, that part of the port has to wait. See Not yet on NuGet.

The base library is the one exception to "package id matches assembly name"

Its package id is Transpose.BCL but its assembly is still Transpose, so Transpose.dll and the runtime assembly name Transpose are unchanged.


Step 1 — Update the project file

Change the SDK and the package references. An h5 project like:

MyProject.csproj
<Project Sdk="h5.Target/*">
  <PropertyGroup>
    <TargetFramework>netstandard2.1</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="h5" Version="*" />
    <PackageReference Include="h5.Core" Version="*" />
    <PackageReference Include="h5.Newtonsoft.Json" Version="*" />
  </ItemGroup>
</Project>

becomes:

MyProject.csproj
<Project Sdk="Transpose.Build.Target/*">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Transpose.BCL" Version="*" />
    <PackageReference Include="Transpose.Core" Version="*" />
    <PackageReference Include="Transpose.Newtonsoft.Json" Version="*" />
  </ItemGroup>
</Project>

Notes:

  • Replace each * with the latest published version — see NuGet Packages.
  • netstandard2.0 and netstandard2.1 both work. The SDK inherits LangVersion=latest; pin <LangVersion> only if you need an older one. (h5 supported a subset of C# 7.x — Transpose is built on Roslyn, so the current language version is available.)
  • If you had <UpdateH5>false</UpdateH5>, drop it. There is no global-tool auto-update to disable.
  • A library you distribute as a package keeps <GeneratePackageOnBuild>true</GeneratePackageOnBuild>. Transpose emits it as a .NET DLL with the compiled JavaScript embedded, which referencing projects consume directly.

Step 2 — Rename h5.json to tps.json

Rename the config file and update the token inside it. The schema is otherwise the same for the settings Transpose supports (output, fileName, html, reflection, resources, outputFormatting, …).

  • output: "$(OutDir)/h5/""$(OutDir)/tps/"
  • Any resource files paths that referenced an h5/… asset folder now reference tps/…. If you keep your web assets under a folder named h5/, rename it to tps/ (or repoint the paths) so the two stay consistent.
tps.json
{
    "output": "$(OutDir)/tps/",
    "fileName": "app.js",
    "outputFormatting": "Both",          // Formatted + Minified
    "resources": [
        { "name": "app.css", "files": [ "tps/assets/css/app.css" ] }
    ]
}

A per-configuration overlay is supported: tps.Release.json (or tps.<Configuration>.json) is merged on top of tps.json for that build. See Build Configurations.

Two behaviour changes in `tps.json`

The config file is read from the project directory only. h5 also looked in an H5/ subfolder next to the project file; Transpose does not.

Property names are now case-sensitive. h5 matched keys case-insensitively. Transpose reads them verbatim, so outputFormatting is read and OutputFormatting is silently ignored. Use camelCase exactly as documented.

Settings that are no longer read

These h5.json settings have no Transpose equivalent and are ignored:

Setting Status
sourceMap Source maps are not emitted yet.
module Module formats (AMD / CommonJS / ES6 / UMD) are not implemented — output is a single global-scope bundle. See Output Types.
locales Not implemented.
beforeBuild / afterBuild Not implemented. Use MSBuild targets in the .csproj.
rules Not implemented.
pluginsPath Compiler plugins are not supported.
generateTypeScript No TypeScript declarations are emitted.
generateDocumentation No JSDoc comments are emitted.
reflection.filter Use [Reflectable(false)] per type instead.
ignoreCast Casts to external types are erased automatically; there is no global switch. See Type Casting.

Resources you load yourself

h5 marked a resource that must be copied but not referenced from index.html by suffixing its name: "name": "lazy-module.js.dontload". Transpose still honours that suffix, and adds a plain flag on the group — use whichever you prefer, since either one alone suppresses the injection:

{
    "resources": [
        { "name": "lazy-module.js", "files": [ "tps/assets/js/lazy-module.js" ], "load": false },
        { "name": "theme-dark.css", "files": [ "tps/assets/css/dark.css" ],      "load": false }
    ]
}

It applies to every resource kind the generated HTML can load — scripts and stylesheets — and it survives packaging: the flag is recorded in the resource manifest embedded in the package DLL, so a project referencing the library extracts the file without auto-loading it either.

Resource keys that are no longer read

extract, inject, header (and its {version} / {year} tokens), silent, removeBom, assembly and remark are ignored. There is also no default-settings entry: in h5, a resources entry without a name supplied defaults for every other entry; in Transpose a name-less entry is just a copy-through group. Repeat output and load on each group instead.

Two changes work in your favour and let you delete boilerplate:

  • Adding a resources section no longer turns off the automatic output. h5 required a library to re-declare its own .js, .min.js, .meta.js and .meta.min.js; Transpose always embeds them, and only suppresses the default embed of the specific bundle a group re-declares.
  • A library no longer has to list its scripts twice to satisfy both build configurations. Every Transpose package ships its compiled JavaScript in a formatted and a pre-minified variant, so a consuming project extracts the right one on its own.

See Resources for the full current reference.

Cleaning the output folder

h5's cleanOutputFolderBeforeBuild and cleanOutputFolderBeforeBuildPattern deleted files matching a glob before compiling. Transpose replaces that with a zero-config cleanOutputFolder (default on): after a successful build it diffs the output folder against exactly the files the build produced and removes only the leftovers — a bundle you renamed, a .min variant a Formatted build no longer emits, a resource you deleted, a stale index.min.html.

Nothing the current build wrote is ever touched, and a build that fails leaves the previous output intact. Drop the old keys; they are no longer read.

To keep hand-placed files that live in the output folder, list them under cleanOutputFolderExclude (glob patterns, the equivalent of h5's ! skip patterns). To disable pruning entirely, set "cleanOutputFolder": false.

tps.json
{
    "output": "$(OutDir)/tps/",
    "cleanOutputFolder": true,                    // the default; set false to keep stale files
    "cleanOutputFolderExclude": [ "favicon.ico", "vendor/*" ]
}

Step 3 — Update source code

The change is almost entirely in using directives:

  • using H5;using Transpose;
  • using H5.Core;using Transpose.Core;
  • using static H5.Core.dom;using static Transpose.Core.dom;
  • Fully-qualified references H5.SomethingTranspose.Something, Transpose.Core.SomethingTranspose.Core.Something.
  • Conditional compilation: #if H5#if TRANSPOSE. Transpose defines the TRANSPOSE symbol during transpilation.
  • If your project sets <DefineConstants>H5</DefineConstants> to mark browser-side code, either keep defining H5 yourself or switch the guards to TRANSPOSE.

Things that do not change, because they are unqualified names or preserved tokens:

  • Codegen attributes: [External], [Name(...)], [Template(...)], [Script(...)], [ObjectLiteral], [GlobalMethods], [Scope], [Enum(...)] — same names, now in namespace Transpose, so a single using Transpose; covers them.
  • Raw JS interop: Script.Write<T>("…") and Script.Write("…") are unchanged.
  • The runtime helper types you call by their short name.
Do NOT blanket-replace `h5` / `H5`

A few tokens are deliberately not library identifiers, and a global search-and-replace will break them:

  • The <h5> HTML tag (and any helper named H5 that emits it) — it is the HTML heading element, not the compiler.
  • Hash locals such as h1h5 (for example in ValueTuple hashing).
  • Any user identifier that merely happens to contain h5.

Prefer targeted replacements — using H5;, using H5.Core;, H5., H5R, #if H5, h5.json, h5.js — over a blind rename.

Hand-written JavaScript and embedded resources

If you ship hand-written JavaScript (via [Script], embedded resource files, or a resources bundle) that reaches into the runtime, update the globals:

  • H5.Transpose. (for example H5.assembly(...)Transpose.assembly(...))
  • H5R.TransposeR.
  • References to the runtime file h5.jstps.js.

Attributes that no longer do anything

[Module] / [ModuleDependency], [Init], [Cast], [Mixin], [Constructor], [Field], [Rules], [Optional], [Virtual], [ToAwait], [Priority], [Immutable] and [ExternalInterface] are not read by Transpose. They still compile, so nothing breaks at build time — but the behaviour they asked for is absent. Two have direct replacements:

  • [Init] → use the standard .NET [ModuleInitializer], which Transpose calls ahead of the entry point.
  • [Cast] → use an ordinary method with a [Template].

[IgnoreCast], [Unbox(false)] and [InlineConst] are accepted but redundant — Transpose's default behaviour already does what they asked for.

See the Attribute Reference for the full status table.

Step 4 — Build and run

There is no global-tool compiler and no compilation server anymore. The Transpose.Build.Target SDK runs the tps CLI once per project as part of a normal dotnet build:

dotnet build

Output lands under bin/<Configuration>/<tfm>/tps/ — a runnable site: the tps.js runtime, your app.js bundle, resources, and index.html, in both formatted and minified variants per outputFormatting. Serve it as before:

cd bin/Debug/netstandard2.0/tps/
dotnet serve --port 5000

To start a project from scratch instead of migrating, follow Getting Started — the dotnet new template is not published yet, so a new project begins from the project file shown there.

Referenced projects are compiled once

When project B references project A, a Transpose site build for B consumes A's already-built package DLL (extracting its embedded JavaScript) rather than recompiling A's sources into B's bundle. dotnet build compiles A first, then B reuses it — so editing B re-transpiles only B.

Two things h5 did not have

  • Incremental builds, on by default via the SDK: a rebuild whose inputs all hash the same does nothing, and a body-only edit reuses the cached JavaScript of every untouched type. Turn it off with <TransposeIncremental>false</TransposeIncremental>.
  • Watch mode: tps --watch rebuilds on every save, serves the site on localhost, and reloads the page — swapping stylesheets in place, without a reload, when only CSS changed.

Behavioural notes and known gaps

Beyond the settings and attributes above, these differences are worth knowing before you port:

  • Source maps for the emitted bundle are not emitted yet. Debug against the emitted JavaScript, which keeps your type and member names — see Exceptions & Debugging.
  • Module formats are not implemented; the output is a single global-scope bundle.
  • Struct value-copy semantics apply only to structs declared in the project being compiled. A struct from a referenced library, a BCL struct (DateTime, TimeSpan, …) and a ValueTuple are copied by reference, so var b = a; b.Inner.V = 9; writes through to a for those. This is a deliberate trade-off — widening it would make every DateTime or tuple assignment allocate.
  • Span<T> does not accept the implicit array conversion. Span<int> s = new int[3]; emits the bare array, so s[0] = 1 fails at runtime. Use the array directly.
  • A positional pattern only resolves members for tuples and records. x is Foo(1, 2) against a type with a hand-written Deconstruct(out …) reads Item1/Item2 instead of calling Deconstruct.
  • Some newer LINQ operators are missingAppend, Prepend, ToHashSet, DistinctBy, Order, TakeLast and friends. Each is a compile error rather than a runtime surprise; see LINQ Support for the substitutions.
  • Transpose.Newtonsoft.Json has a documented set of divergences from Json.NET. The one most likely to affect a client and server that share DTOs: a long is serialized as a JSON string. See JSON Serialization.
  • Retyped / Bridge packages are not supported — unchanged from h5.
  • Reference resolution beyond the NuGet cache (<Reference HintPath> and the tps.json references / referencesPath settings) is partial; the tps --reference flag covers the common cases.
  • MSBuild evaluation stays deliberately shallow. <Import> is followed transitively, so a shared project's .projitems is picked up — but conditions, arbitrary $(…) properties, Directory.Build.props and item metadata are not evaluated.

If you hit a construct that transpiled under h5 but is rejected or emitted differently by Transpose, please open an issue with a minimal repro.

© 2026 Curiosity. All rights reserved.