diff --git a/eng/pipelines/mono/templates/workloads-build.yml b/eng/pipelines/mono/templates/workloads-build.yml
index a6b6e759306880..0b6e057e8222d9 100644
--- a/eng/pipelines/mono/templates/workloads-build.yml
+++ b/eng/pipelines/mono/templates/workloads-build.yml
@@ -65,6 +65,7 @@ jobs:
IntermediateArtifacts/MonoRuntimePacks/Shipping/Microsoft.NET.Runtime.MonoTargets.Sdk*.nupkg
IntermediateArtifacts/MonoRuntimePacks/Shipping/Microsoft.NET.Runtime.MonoAOTCompiler.Task*.nupkg
IntermediateArtifacts/MonoRuntimePacks/Shipping/Microsoft.NET.Runtime.WebAssembly.Sdk*.nupkg
+ IntermediateArtifacts/MonoRuntimePacks/Shipping/Microsoft.NET.Runtime.WebAssembly.Templates*.nupkg
- task: CopyFiles@2
displayName: Flatten packages
diff --git a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Manifest/WorkloadManifest.json.in b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Manifest/WorkloadManifest.json.in
index e34ff6d173b85a..b0fd53947e1351 100644
--- a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Manifest/WorkloadManifest.json.in
+++ b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Manifest/WorkloadManifest.json.in
@@ -8,6 +8,7 @@
"description": ".NET WebAssembly build tools",
"packs": [
"Microsoft.NET.Runtime.WebAssembly.Sdk",
+ "Microsoft.NET.Runtime.WebAssembly.Templates",
"Microsoft.NETCore.App.Runtime.Mono.browser-wasm",
"Microsoft.NETCore.App.Runtime.AOT.Cross.browser-wasm"
],
@@ -140,6 +141,10 @@
"kind": "Sdk",
"version": "${PackageVersion}"
},
+ "Microsoft.NET.Runtime.WebAssembly.Templates": {
+ "kind": "template",
+ "version": "${PackageVersion}"
+ },
"Microsoft.NETCore.App.Runtime.Mono.android-arm": {
"kind": "framework",
"version": "${PackageVersion}"
diff --git a/src/mono/nuget/mono-packages.proj b/src/mono/nuget/mono-packages.proj
index a56c63f8827642..e57f6855d975f9 100644
--- a/src/mono/nuget/mono-packages.proj
+++ b/src/mono/nuget/mono-packages.proj
@@ -7,6 +7,7 @@
+
diff --git a/src/mono/wasm/README.md b/src/mono/wasm/README.md
index 37d5fa29643445..a016b9a1aa3001 100644
--- a/src/mono/wasm/README.md
+++ b/src/mono/wasm/README.md
@@ -161,7 +161,25 @@ To build and run the samples with AOT, add `/p:RunAOTCompilation=true` to the ab
Also check [bench](../sample/wasm/browser-bench/README.md) sample to measure mono/wasm runtime performance.
-### Upgrading Emscripten
+## Templates
+
+The wasm templates, located in the `templates` directory, are templates for `dotnet new`, VS and VS for Mac. They are packaged and distributed as part of the `wasm-tools` workload. We have 2 templates, `wasmbrowser` and `wasmconsole`, for browser and console WebAssembly applications.
+
+For details about using `dotnet new` see the dotnet tool [documentation](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-new).
+
+To test changes in the templates, use `dotnet new -i `.
+
+Example use of the `wasmconsole` template:
+
+ > dotnet new wasmconsole
+ > dotnet publish
+ > cd bin/Debug/net7.0/browser-wasm/AppBundle
+ > node main.cjs
+ mono_wasm_runtime_ready fe00e07a-5519-4dfe-b35a-f867dbaf2e28
+ Hello World!
+ Args:
+
+## Upgrading Emscripten
Bumping Emscripten version involves these steps:
diff --git a/src/mono/wasm/templates/Microsoft.NET.Runtime.WebAssembly.Templates.csproj b/src/mono/wasm/templates/Microsoft.NET.Runtime.WebAssembly.Templates.csproj
new file mode 100644
index 00000000000000..77f730d55b23db
--- /dev/null
+++ b/src/mono/wasm/templates/Microsoft.NET.Runtime.WebAssembly.Templates.csproj
@@ -0,0 +1,33 @@
+
+
+
+ Template
+ Microsoft.NET.Runtime.WebAssembly.Templates
+ WebAssembly Templates
+ Microsoft
+ Templates to create WebAssembly projects.
+ dotnet-new;templates
+
+ net6.0
+
+ true
+ false
+ content
+ $(NoWarn);NU5128
+ true
+
+
+
+
+
+
+
+
+
+
+
+ $(ProductVersion)
+
+
+
+
diff --git a/src/mono/wasm/templates/templates/browser/.template.config/template.json b/src/mono/wasm/templates/templates/browser/.template.config/template.json
new file mode 100644
index 00000000000000..e9736c68a1ba9c
--- /dev/null
+++ b/src/mono/wasm/templates/templates/browser/.template.config/template.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "http://json.schemastore.org/template",
+ "author": "Microsoft",
+ "classifications": [ "Web", "WebAssembly", "Browser" ],
+ "identity": "WebAssembly.Browser",
+ "name": "WebAssembly Browser App",
+ "shortName": "wasmbrowser",
+ "tags": {
+ "language": "C#",
+ "type": "project"
+ }
+}
diff --git a/src/mono/wasm/templates/templates/browser/Program.cs b/src/mono/wasm/templates/templates/browser/Program.cs
new file mode 100644
index 00000000000000..baa90549b70363
--- /dev/null
+++ b/src/mono/wasm/templates/templates/browser/Program.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Runtime.CompilerServices;
+
+Console.WriteLine ("Hello, Console!");
+
+public class MyClass {
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static string CallMeFromJS()
+ {
+ return "Hello, World!";
+ }
+}
diff --git a/src/mono/wasm/templates/templates/browser/browser.csproj b/src/mono/wasm/templates/templates/browser/browser.csproj
new file mode 100644
index 00000000000000..6b36bc95f7f585
--- /dev/null
+++ b/src/mono/wasm/templates/templates/browser/browser.csproj
@@ -0,0 +1,17 @@
+
+
+ net7.0
+ wasm
+ Browser
+ browser-wasm
+ true
+ main.js
+ Exe
+ true
+
+
+
+
+
+
+
diff --git a/src/mono/wasm/templates/templates/browser/index.html b/src/mono/wasm/templates/templates/browser/index.html
new file mode 100644
index 00000000000000..3cc12e3212d6b5
--- /dev/null
+++ b/src/mono/wasm/templates/templates/browser/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ Sample ES6
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/mono/wasm/templates/templates/browser/main.js b/src/mono/wasm/templates/templates/browser/main.js
new file mode 100644
index 00000000000000..56c9b350313ec1
--- /dev/null
+++ b/src/mono/wasm/templates/templates/browser/main.js
@@ -0,0 +1,13 @@
+import createDotnetRuntime from './dotnet.js'
+
+try {
+ const { MONO, BINDING, Module, RuntimeBuildInfo } = await createDotnetRuntime();
+ const managedMethod = BINDING.bind_static_method("[browser] MyClass:CallMeFromJS");
+ const text = managedMethod();
+ document.getElementById("out").innerHTML = `${text}`;
+
+ await MONO.mono_run_main("browser.dll", []);
+} catch (err) {
+ console.log(`WASM ERROR ${err}`);
+ document.getElementById("out").innerHTML = `error: ${err}`;
+}
diff --git a/src/mono/wasm/templates/templates/console/.template.config/template.json b/src/mono/wasm/templates/templates/console/.template.config/template.json
new file mode 100644
index 00000000000000..0a67c332bb7d7e
--- /dev/null
+++ b/src/mono/wasm/templates/templates/console/.template.config/template.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "http://json.schemastore.org/template",
+ "author": "Microsoft",
+ "classifications": [ "Web", "WebAssembly", "Console" ],
+ "identity": "WebAssembly.Console",
+ "name": "WebAssembly Console App",
+ "shortName": "wasmconsole",
+ "tags": {
+ "language": "C#",
+ "type": "project"
+ }
+}
diff --git a/src/mono/wasm/templates/templates/console/Program.cs b/src/mono/wasm/templates/templates/console/Program.cs
new file mode 100644
index 00000000000000..3a242248ba2090
--- /dev/null
+++ b/src/mono/wasm/templates/templates/console/Program.cs
@@ -0,0 +1,9 @@
+using System;
+using System.Threading.Tasks;
+
+Console.WriteLine("Hello World!");
+
+Console.WriteLine("Args:");
+for (int i = 0; i < args.Length; i++) {
+ Console.WriteLine($" args[{i}] = {args[i]}");
+}
diff --git a/src/mono/wasm/templates/templates/console/README.md b/src/mono/wasm/templates/templates/console/README.md
new file mode 100644
index 00000000000000..0153a8574c1a94
--- /dev/null
+++ b/src/mono/wasm/templates/templates/console/README.md
@@ -0,0 +1,7 @@
+Node/CommonJS console App
+
+Run the published application like:
+
+ node main.cjs
+
+in `bin/$(Configuration)/net7.0/browser-wasm/AppBundle` directory.
diff --git a/src/mono/wasm/templates/templates/console/console.csproj b/src/mono/wasm/templates/templates/console/console.csproj
new file mode 100644
index 00000000000000..fad622389dc305
--- /dev/null
+++ b/src/mono/wasm/templates/templates/console/console.csproj
@@ -0,0 +1,12 @@
+
+
+ net7.0
+ wasm
+ Browser
+ browser-wasm
+ true
+ main.cjs
+ Exe
+ false
+
+
diff --git a/src/mono/wasm/templates/templates/console/main.cjs b/src/mono/wasm/templates/templates/console/main.cjs
new file mode 100644
index 00000000000000..18823502c64fa7
--- /dev/null
+++ b/src/mono/wasm/templates/templates/console/main.cjs
@@ -0,0 +1,8 @@
+const createDotnetRuntime = require("./dotnet.js");
+
+async function main() {
+ const { MONO } = await createDotnetRuntime();
+ const app_args = process.argv.slice(2);
+ await MONO.mono_run_main_and_exit("console.dll", app_args);
+};
+main();
diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs
index ec37a933562bd7..4d3abaa401abec 100644
--- a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs
+++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs
@@ -127,7 +127,8 @@ protected string RunAndTestWasmApp(BuildArgs buildArgs,
string? buildDir = null,
int expectedExitCode = 0,
string? args = null,
- Dictionary? envVars = null)
+ Dictionary? envVars = null,
+ string targetFramework = "net6.0")
{
buildDir ??= _projectDir;
envVars ??= new();
@@ -144,7 +145,7 @@ protected string RunAndTestWasmApp(BuildArgs buildArgs,
envVars[kvp.Key] = kvp.Value;
}
- string bundleDir = Path.Combine(GetBinDir(baseDir: buildDir, config: buildArgs.Config), "AppBundle");
+ string bundleDir = Path.Combine(GetBinDir(baseDir: buildDir, config: buildArgs.Config, targetFramework: targetFramework), "AppBundle");
(string testCommand, string extraXHarnessArgs) = host switch
{
RunHost.V8 => ("wasm test", "--js-file=test-main.js --engine=V8 -v trace"),
@@ -341,8 +342,8 @@ protected static BuildArgs ExpandBuildArgs(BuildArgs buildArgs, string extraProp
if (options.ExpectSuccess)
{
- string bundleDir = Path.Combine(GetBinDir(config: buildArgs.Config), "AppBundle");
- AssertBasicAppBundle(bundleDir, buildArgs.ProjectName, buildArgs.Config, options.HasIcudt, options.DotnetWasmFromRuntimePack ?? !buildArgs.AOT);
+ string bundleDir = Path.Combine(GetBinDir(config: buildArgs.Config, targetFramework: options.TargetFramework ?? "net6.0"), "AppBundle");
+ AssertBasicAppBundle(bundleDir, buildArgs.ProjectName, buildArgs.Config, options.MainJS ?? "test-main.js", options.HasV8Script, options.HasIcudt, options.DotnetWasmFromRuntimePack ?? !buildArgs.AOT);
}
if (options.UseCache)
@@ -371,6 +372,18 @@ public void InitBlazorWasmProjectDir(string id)
File.Copy(Path.Combine(BuildEnvironment.TestDataPath, "Blazor.Directory.Build.targets"), Path.Combine(_projectDir, "Directory.Build.targets"));
}
+ public string CreateWasmTemplateProject(string id, string template = "wasmbrowser")
+ {
+ InitPaths(id);
+ InitProjectDir(id);
+ new DotNetCommand(s_buildEnv, useDefaultArgs: false)
+ .WithWorkingDirectory(_projectDir!)
+ .ExecuteWithCapturedOutput($"new {template}")
+ .EnsureSuccessful();
+
+ return Path.Combine(_projectDir!, $"{id}.csproj");
+ }
+
public string CreateBlazorWasmTemplateProject(string id)
{
InitBlazorWasmProjectDir(id);
@@ -476,19 +489,19 @@ static void AssertRuntimePackPath(string buildOutput)
throw new XunitException($"Runtime pack path doesn't match.{Environment.NewLine}Expected: {s_buildEnv.RuntimePackDir}{Environment.NewLine}Actual: {actualPath}");
}
- protected static void AssertBasicAppBundle(string bundleDir, string projectName, string config, bool hasIcudt=true, bool dotnetWasmFromRuntimePack=true)
+ protected static void AssertBasicAppBundle(string bundleDir, string projectName, string config, string mainJS, bool hasV8Script, bool hasIcudt=true, bool dotnetWasmFromRuntimePack=true)
{
AssertFilesExist(bundleDir, new []
{
"index.html",
- "test-main.js",
+ mainJS,
"dotnet.timezones.blat",
"dotnet.wasm",
"mono-config.json",
- "dotnet.js",
- "run-v8.sh"
+ "dotnet.js"
});
+ AssertFilesExist(bundleDir, new[] { "run-v8.sh" }, expectToExist: hasV8Script);
AssertFilesExist(bundleDir, new[] { "icudt.dat" }, expectToExist: hasIcudt);
string managedDir = Path.Combine(bundleDir, "managed");
@@ -866,7 +879,10 @@ public record BuildProjectOptions
bool CreateProject = true,
bool Publish = true,
bool BuildOnlyAfterPublish = true,
+ bool HasV8Script = true,
string? Verbosity = null,
- string? Label = null
+ string? Label = null,
+ string? TargetFramework = null,
+ string? MainJS = null
);
}
diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmTemplateTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmTemplateTests.cs
new file mode 100644
index 00000000000000..d3684c3cfe3db9
--- /dev/null
+++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/WasmTemplateTests.cs
@@ -0,0 +1,107 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+#nullable enable
+
+namespace Wasm.Build.Tests
+{
+ public class WasmTemplateTests : BuildTestBase
+ {
+ public WasmTemplateTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
+ : base(output, buildContext)
+ {
+ }
+
+ [Theory]
+ [InlineData("Debug")]
+ [InlineData("Release")]
+ public void BrowserBuildThenPublish(string config)
+ {
+ string id = $"{config}_{Path.GetRandomFileName()}";
+ string projectName = $"browser";
+ CreateWasmTemplateProject(id, "wasmbrowser");
+
+ var buildArgs = new BuildArgs(projectName, config, false, id, null);
+ buildArgs = ExpandBuildArgs(buildArgs);
+
+ BuildProject(buildArgs,
+ id: id,
+ new BuildProjectOptions(
+ DotnetWasmFromRuntimePack: false,
+ CreateProject: false,
+ HasV8Script: false,
+ MainJS: "main.js",
+ Publish: false,
+ TargetFramework: "net7.0"
+ ));
+
+ if (!_buildContext.TryGetBuildFor(buildArgs, out BuildProduct? product))
+ throw new XunitException($"Test bug: could not get the build product in the cache");
+
+ File.Move(product!.LogFile, Path.ChangeExtension(product.LogFile!, ".first.binlog"));
+
+ _testOutput.WriteLine($"{Environment.NewLine}Publishing with no changes ..{Environment.NewLine}");
+ Console.WriteLine($"{Environment.NewLine}Publishing with no changes ..{Environment.NewLine}");
+
+ BuildProject(buildArgs,
+ id: id,
+ new BuildProjectOptions(
+ DotnetWasmFromRuntimePack: false,
+ CreateProject: false,
+ HasV8Script: false,
+ MainJS: "main.js",
+ Publish: true,
+ TargetFramework: "net7.0",
+ UseCache: false));
+ }
+
+ [Theory]
+ [InlineData("Debug")]
+ [InlineData("Release")]
+ public void ConsoleBuildThenPublish(string config)
+ {
+ string id = $"{config}_{Path.GetRandomFileName()}";
+ string projectName = $"console";
+ CreateWasmTemplateProject(id, "wasmconsole");
+
+ var buildArgs = new BuildArgs(projectName, config, false, id, null);
+ buildArgs = ExpandBuildArgs(buildArgs);
+
+ BuildProject(buildArgs,
+ id: id,
+ new BuildProjectOptions(
+ DotnetWasmFromRuntimePack: false,
+ CreateProject: false,
+ HasV8Script: false,
+ MainJS: "main.mjs",
+ Publish: false,
+ TargetFramework: "net7.0"
+ ));
+
+ if (!_buildContext.TryGetBuildFor(buildArgs, out BuildProduct? product))
+ throw new XunitException($"Test bug: could not get the build product in the cache");
+
+ File.Move(product!.LogFile, Path.ChangeExtension(product.LogFile!, ".first.binlog"));
+
+ _testOutput.WriteLine($"{Environment.NewLine}Publishing with no changes ..{Environment.NewLine}");
+ Console.WriteLine($"{Environment.NewLine}Publishing with no changes ..{Environment.NewLine}");
+
+ BuildProject(buildArgs,
+ id: id,
+ new BuildProjectOptions(
+ DotnetWasmFromRuntimePack: false,
+ CreateProject: false,
+ HasV8Script: false,
+ MainJS: "main.mjs",
+ Publish: true,
+ TargetFramework: "net7.0",
+ UseCache: false));
+ }
+ }
+}