diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7ea220dc5b7b..3bc736e77a29 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -94,6 +94,7 @@
+
diff --git a/eng/Versions.props b/eng/Versions.props
index 60d378272929..c90f34598609 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -142,6 +142,7 @@
8.0.2
8.0.0
+ 5.9.3
4.18.4
1.3.2
8.0.0-beta.23607.1
diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.TypeScript.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.TypeScript.targets
new file mode 100644
index 000000000000..ccf65443d5ec
--- /dev/null
+++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.TypeScript.targets
@@ -0,0 +1,86 @@
+
+
+
+
+
+ RegisterTypeScriptStaticWebAssets;
+ $(ResolveStaticWebAssetsInputsDependsOn)
+
+
+
+
+
+ <_TypeScriptSwaManifestPath>$(_StaticWebAssetsManifestBase)staticwebassets.typescript.files.txt
+
+
+
+
+
+
+ <_TypeScriptWwwrootAssets Include="@(GeneratedJavascript)"
+ Condition="$([System.String]::new('%(Identity)').Contains('wwwroot')) And Exists('%(Identity)')" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets
index c3e5fad9ab49..8b5dfd698a67 100644
--- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets
+++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets
@@ -538,7 +538,7 @@ Copyright (c) .NET Foundation. All rights reserved.
-
+
@@ -786,4 +786,6 @@ Copyright (c) .NET Foundation. All rights reserved.
+
+
diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj
index 377507009b2d..447138e1e272 100644
--- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj
+++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj
@@ -34,6 +34,10 @@
<_Parameter1>DefaultTestBaselinePackageVersion
<_Parameter2>5.0
+
+ <_Parameter1>MicrosoftTypeScriptMSBuildPackageVersion
+ <_Parameter2>$(MicrosoftTypeScriptMSBuildPackageVersion)
+
diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/TypeScriptIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/TypeScriptIntegrationTest.cs
new file mode 100644
index 000000000000..9c85b45c6e81
--- /dev/null
+++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/TypeScriptIntegrationTest.cs
@@ -0,0 +1,286 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable disable
+
+using System.Reflection;
+using Microsoft.AspNetCore.StaticWebAssets.Tasks;
+
+namespace Microsoft.NET.Sdk.StaticWebAssets.Tests;
+
+public class TypeScriptIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest
+{
+ public string TypeScriptMSBuildPackageVersion { get; }
+
+ public TypeScriptIntegrationTest(ITestOutputHelper log) : base(log, nameof(TypeScriptIntegrationTest))
+ {
+ var testAssemblyMetadata = TestAssembly.GetCustomAttributes();
+ TypeScriptMSBuildPackageVersion = testAssemblyMetadata.SingleOrDefault(a => a.Key == "MicrosoftTypeScriptMSBuildPackageVersion")?.Value ?? "5.9.3";
+ }
+
+ [Fact]
+ public void Build_RegistersTypeScriptOutputsAsStaticWebAssets()
+ {
+ var testAsset = "RazorClassLibrary";
+ ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
+
+ SetupTypeScriptProject(ProjectDirectory);
+
+ var build = CreateBuildCommand(ProjectDirectory);
+ ExecuteCommand(build).Should().Pass();
+
+ var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
+
+ // Verify the TypeScript manifest was created
+ var manifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.typescript.files.txt");
+ new FileInfo(manifestPath).Should().Exist();
+ // Verify the static web assets manifest contains the TypeScript outputs
+ var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json");
+ var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(finalPath));
+
+ buildManifest.Should().NotBeNull();
+ buildManifest.Assets.Should().Contain(a => a.RelativePath.EndsWith("app.js"));
+ }
+
+ [Fact]
+ public void Build_TypeScriptOutputsAreProperlyCompressed()
+ {
+ var testAsset = "RazorClassLibrary";
+ ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
+
+ SetupTypeScriptProject(ProjectDirectory);
+
+ // Enable compression
+ ProjectDirectory.WithProjectChanges(document =>
+ {
+ var propertyGroup = document.Root.Descendants()
+ .FirstOrDefault(e => e.Name.LocalName == "PropertyGroup");
+ propertyGroup?.Add(new XElement("CompressionEnabled", "true"));
+ });
+
+ var build = CreateBuildCommand(ProjectDirectory);
+ ExecuteCommand(build).Should().Pass();
+
+ var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
+
+ // Verify compression was applied
+ var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json");
+ var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(finalPath));
+
+ // Should have compressed versions
+ buildManifest.Assets.Should().Contain(a => a.RelativePath.EndsWith("app.js.gz") || a.RelativePath.EndsWith("app.js.br"));
+ }
+
+ [Fact]
+ public void Rebuild_SucceedsWithTypeScriptOutputs()
+ {
+ var testAsset = "RazorClassLibrary";
+ ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
+
+ SetupTypeScriptProject(ProjectDirectory);
+
+ // First build
+ var build = CreateBuildCommand(ProjectDirectory);
+ ExecuteCommand(build).Should().Pass();
+
+ // Rebuild (clean + build)
+ var rebuild = CreateRebuildCommand(ProjectDirectory);
+ var rebuildResult = ExecuteCommand(rebuild);
+ rebuildResult.Should().Pass();
+
+ var intermediateOutputPath = rebuild.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
+
+ // Verify static web assets are still correctly registered after rebuild
+ var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json");
+ var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(finalPath));
+
+ buildManifest.Should().NotBeNull();
+ buildManifest.Assets.Should().Contain(a => a.RelativePath.EndsWith("app.js"));
+ }
+
+ [Fact]
+ public async Task Build_IncrementalBuild_WorksCorrectly()
+ {
+ var testAsset = "RazorClassLibrary";
+ ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
+
+ SetupTypeScriptProject(ProjectDirectory);
+
+ // First build
+ var build = CreateBuildCommand(ProjectDirectory);
+ ExecuteCommand(build).Should().Pass();
+
+ var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
+ var manifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.typescript.files.txt");
+
+ var firstBuildManifestTime = File.GetLastWriteTime(manifestPath);
+
+ // Wait a bit and do incremental build
+ await Task.Delay(100);
+
+ build = CreateBuildCommand(ProjectDirectory);
+ ExecuteCommand(build).Should().Pass();
+
+ // Manifest should not have changed (WriteOnlyWhenDifferent)
+ var secondBuildManifestTime = File.GetLastWriteTime(manifestPath);
+ secondBuildManifestTime.Should().Be(firstBuildManifestTime);
+ }
+
+ [Fact]
+ public void Build_ModifyTypeScriptFile_UpdatesStaticWebAssets()
+ {
+ var testAsset = "RazorClassLibrary";
+ ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
+
+ SetupTypeScriptProject(ProjectDirectory);
+
+ // First build
+ var build = CreateBuildCommand(ProjectDirectory);
+ ExecuteCommand(build).Should().Pass();
+
+ // Modify the TypeScript file
+ var tsFilePath = Path.Combine(ProjectDirectory.TestRoot, "Scripts", "app.ts");
+ File.WriteAllText(tsFilePath, "console.log('Modified!');");
+
+ // Rebuild
+ build = CreateBuildCommand(ProjectDirectory);
+ ExecuteCommand(build).Should().Pass();
+
+ var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
+
+ // Verify the output was updated
+ var jsFilePath = Path.Combine(ProjectDirectory.TestRoot, "wwwroot", "app.js");
+ new FileInfo(jsFilePath).Should().Exist();
+ File.ReadAllText(jsFilePath).Should().Contain("Modified");
+ }
+
+ [Fact]
+ public void Publish_IncludesTypeScriptOutputs()
+ {
+ var testAsset = "RazorClassLibrary";
+ ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
+
+ SetupTypeScriptProject(ProjectDirectory);
+
+ var publish = CreatePublishCommand(ProjectDirectory);
+ ExecuteCommand(publish).Should().Pass();
+
+ var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
+
+ // Verify publish manifest includes TypeScript outputs
+ var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json");
+ var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(finalPath));
+
+ publishManifest.Should().NotBeNull();
+ publishManifest.Assets.Should().Contain(a => a.RelativePath.EndsWith("app.js"));
+ }
+
+ [Fact]
+ public void Clean_ThenBuild_SucceedsWithTypeScriptOutputs()
+ {
+ var testAsset = "RazorClassLibrary";
+ ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
+
+ SetupTypeScriptProject(ProjectDirectory);
+
+ // First build
+ var build = CreateBuildCommand(ProjectDirectory);
+ ExecuteCommand(build).Should().Pass();
+
+ // Clean
+ var clean = new CleanCommand(Log, ProjectDirectory.Path);
+ clean.WithWorkingDirectory(ProjectDirectory.TestRoot);
+ ExecuteCommand(clean).Should().Pass();
+
+ // Build again
+ build = CreateBuildCommand(ProjectDirectory);
+ var result = ExecuteCommand(build);
+ result.Should().Pass();
+
+ var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
+
+ // Verify static web assets are correctly registered
+ var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json");
+ var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(finalPath));
+
+ buildManifest.Should().NotBeNull();
+ buildManifest.Assets.Should().Contain(a => a.RelativePath.EndsWith("app.js"));
+ }
+
+ [Fact]
+ public void Build_TypeScriptDisabled_DoesNotRegisterAssets()
+ {
+ var testAsset = "RazorClassLibrary";
+ ProjectDirectory = CreateAspNetSdkTestAsset(testAsset);
+
+ SetupTypeScriptProject(ProjectDirectory, enableTypeScript: false);
+
+ var build = CreateBuildCommand(ProjectDirectory);
+ ExecuteCommand(build).Should().Pass();
+
+ var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
+
+ // TypeScript manifest should not exist when disabled
+ var manifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.typescript.files.txt");
+ new FileInfo(manifestPath).Should().NotExist();
+ }
+
+ ///
+ /// Sets up a test project with TypeScript configuration using the
+ /// Microsoft.TypeScript.MSBuild NuGet package.
+ ///
+ private void SetupTypeScriptProject(TestAsset projectDirectory, bool enableTypeScript = true)
+ {
+ // Create Scripts folder and TypeScript file
+ var scriptsDir = Path.Combine(projectDirectory.TestRoot, "Scripts");
+ Directory.CreateDirectory(scriptsDir);
+ File.WriteAllText(Path.Combine(scriptsDir, "app.ts"), "console.log('Hello from TypeScript!');");
+
+ // Create tsconfig.json that outputs to wwwroot
+ var tsconfig = @"{
+ ""compilerOptions"": {
+ ""target"": ""ES2020"",
+ ""module"": ""ES2020"",
+ ""outDir"": ""wwwroot"",
+ ""rootDir"": ""Scripts"",
+ ""strict"": true,
+ ""sourceMap"": true
+ },
+ ""include"": [""Scripts/**/*""]
+}";
+ File.WriteAllText(Path.Combine(projectDirectory.TestRoot, "tsconfig.json"), tsconfig);
+
+ // Ensure wwwroot exists
+ Directory.CreateDirectory(Path.Combine(projectDirectory.TestRoot, "wwwroot"));
+
+ // Modify project to add the TypeScript MSBuild package
+ projectDirectory.WithProjectChanges(document =>
+ {
+ var root = document.Root;
+ var ns = root.Name.Namespace;
+
+ // Add PropertyGroup with TypeScript output directory
+ var propertyGroup = root.Descendants()
+ .FirstOrDefault(e => e.Name.LocalName == "PropertyGroup");
+
+ if (propertyGroup != null)
+ {
+ propertyGroup.Add(new XElement(ns + "TypeScriptOutDir", "wwwroot"));
+ }
+
+ if (enableTypeScript)
+ {
+ // Add Microsoft.TypeScript.MSBuild package reference
+ var itemGroup = new XElement(ns + "ItemGroup",
+ new XElement(ns + "PackageReference",
+ new XAttribute("Include", "Microsoft.TypeScript.MSBuild"),
+ new XAttribute("Version", TypeScriptMSBuildPackageVersion),
+ new XElement(ns + "PrivateAssets", "all"),
+ new XElement(ns + "IncludeAssets", "runtime; build; native; contentfiles; analyzers; buildtransitive")
+ )
+ );
+ root.Add(itemGroup);
+ }
+ });
+ }
+}