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:
<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:
<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.0andnetstandard2.1both work. The SDK inheritsLangVersion=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
filespaths that referenced anh5/…asset folder now referencetps/…. If you keep your web assets under a folder namedh5/, rename it totps/(or repoint the paths) so the two stay consistent.
{
"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
resourcessection no longer turns off the automatic output. h5 required a library to re-declare its own.js,.min.js,.meta.jsand.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.
{
"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.Something→Transpose.Something,Transpose.Core.Something→Transpose.Core.Something. - Conditional compilation:
#if H5→#if TRANSPOSE. Transpose defines theTRANSPOSEsymbol during transpilation. - If your project sets
<DefineConstants>H5</DefineConstants>to mark browser-side code, either keep definingH5yourself or switch the guards toTRANSPOSE.
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 namespaceTranspose, so a singleusing Transpose;covers them. - Raw JS interop:
Script.Write<T>("…")andScript.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 namedH5that emits it) — it is the HTML heading element, not the compiler. - Hash locals such as
h1…h5(for example inValueTuplehashing). - 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 exampleH5.assembly(...)→Transpose.assembly(...))H5R.→TransposeR.- References to the runtime file
h5.js→tps.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 --watchrebuilds 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 aValueTupleare copied by reference, sovar b = a; b.Inner.V = 9;writes through toafor those. This is a deliberate trade-off — widening it would make everyDateTimeor tuple assignment allocate. Span<T>does not accept the implicit array conversion.Span<int> s = new int[3];emits the bare array, sos[0] = 1fails 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-writtenDeconstruct(out …)readsItem1/Item2instead of callingDeconstruct. - Some newer LINQ operators are missing —
Append,Prepend,ToHashSet,DistinctBy,Order,TakeLastand friends. Each is a compile error rather than a runtime surprise; see LINQ Support for the substitutions. Transpose.Newtonsoft.Jsonhas a documented set of divergences from Json.NET. The one most likely to affect a client and server that share DTOs: alongis 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 thetps.jsonreferences/referencesPathsettings) is partial; thetps --referenceflag covers the common cases. - MSBuild evaluation stays deliberately shallow.
<Import>is followed transitively, so a shared project's.projitemsis picked up — but conditions, arbitrary$(…)properties,Directory.Build.propsand 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.