TypeScript
TypeScript copied to clipboard
Visual Studio: Changes in .ts/.tsx files break up-to-date check until full rebuild.
Bug Report
⚠️ ⚠️ Very important performance problem for any TypeScript developer in Visual Studio, specially when using more than one probject ⚠️ ⚠️
TypeScript files afect the build, but do not produce any output in bin/obj, because of this, after succesfully compiling a Web Application (<Project Sdk="Microsoft.NET.Sdk.Web">) that uses the Nuget Microsoft.TypeScript.MSBuild to compile some ts file, if you modify a ts file, the project will be forever ignored by the up-to-date check until a .cs file is changes and a build updates the output dll.
🔎 Search Terms
Visual Studio, UpToDateCheckInput, Microsoft.TypeScript.MSBuild, FastUpToDate, WARNING: Potential build performance issue in
🕗 Version & Regression Information
Visual Studio 2022 64-bits 17.5.3 Microsoft.TypeScript.MSBuild 5.0.4 ( but I think this is old issue)
⏯ Playground Link
I was not sure if the error belongs to TypeScript or VS team. I explained the issue in full here:
https://github.com/dotnet/project-system/issues/8981
The bug here is that the project will never be considered up-to-date, meaning a build will always be scheduled, which can hurt developer inner-loop productivity.
The issues appears to be in the Microsoft.TypeScript.MSBuild package. It should not be exposing TypeScriptCompile MSBuild items as inputs to the up-to-date check, as they do not contribute to the primary MSBuild output (the .dll file).
Perhaps there's a ProjectSchemaDefinition with something like this:
<ProjectSchemaDefinitions xmlns="http://schemas.microsoft.com/build/2009/properties">
<ItemType Name="TypeScriptCompile" UpToDateCheckInput="True" />
</ProjectSchemaDefinitions>
Instead, TypeScript inputs and outputs should be added to their own "set" as described in https://github.com/dotnet/project-system/blob/main/docs/up-to-date-check.md#grouping-inputs-and-outputs-into-sets
Feel free to reach out if anything's unclear.
I'm going to take a look. Can someone provide a binlog that exhibits the issue? I'm not seeing UpToDateCheckInput anywhere in the targets and I don't thing we have TypeScriptCompile in the web sdk (although I didn't check that and it may be the issue).
@joj there are repro steps in https://github.com/dotnet/project-system/issues/8981
You can get a binlog from VS's design-time builds as described in https://github.com/dotnet/project-system-tools#getting-higher-fidelity-logs-from-vs-vs2022-onwards
I don't thing we have TypeScriptCompile in the web sdk
It would be worth checking if the problem exists in a regular .NET Console App that takes a reference on the Microsoft.TypeScript.MSBuild package, with .ts files added. It might be the problem exists solely in that package.
One challenge I can see in solving this is that you'll need to identify an output for TypeScript file(s). Either one per input, or one for all of them (if bundling somehow).
One challenge I can see in solving this is that you'll need to identify an output for TypeScript file(s). Either one per input, or one for all of them (if bundling somehow).
Maybe is enough to use .tsbuildinfo as the output file, or produce warning if incremental is not active?
Checking the binlogs, that warning seems to be wrong. CoreCompile is not happening again, CompileTypeScript is not happening again and we have no TypeScriptCompile items in the build. Also, the timestamp of the dll is not changing in that case. I have 3 binlogs: one for a build where there was no recompile triggerred (no warning), one where I changed the ts and there was a rebuild, one right after that with no change but a build reported. In no case was the dll actually changed as far as I can tell. I'm attaching them as a zip. binlogs.zip
@olmobrutall are you seeing the timestamp of the dll (or any other factor) changing in these cases? Maybe I'm checking it wrong :)
Are you able to see the problems activating https://github.com/dotnet/project-system/blob/main/docs/up-to-date-check.md#sdk-style-projects?
I'm not used to binlog but maybe this brings some light:
The way I see it is there are three moving parts:
-
Visual studio up-to-date check
- Input: All cs and ts files
- Output: Only the dll/pdb
-
MSBuild / CSC
- CS files
- Output: Only the dll/pdb
-
MSBuild / TSC
- TS files
- Output: js, sourcemaps etc...
In the csproj / Microsoft.TypeScript.MSBuild.targets the TS files should not be inclided into the default UpToDateCheckInput, because otherwise is compared with the default UpToDateCheckOutput (the dll) that doesn't get updated because the CS compiler doesn't see any change in his CS files.
I believe that setting is default for me, I checked and it was set. I'm seeing the same output you're seeing, but the behavior seems right. The output implies something different from what I'm seeing happening. @drewnoakes will be able to check the binlogs (and also, if you have the time I recommend them, they are an amazing tool :)). The quick check for you is: are you seeing the dll change at all (meaning, timestamps or hashes or something) in the case when the ts files are not changing and you still don't get 1 up-to-date in the log?
Another small thing: there is no (immediate) way of making msbuild to just CSC or TSC. Clarifying: you can do that if you want, but the behavior is for compile to do both. The thing is TSC will not run if ts files are not changed in relation to js files, and dotnet has a whole bunch of very complex rules. From what I'm seeing in the logs, those behaviors look correct. The output is saying something else, though, and we still need to understand that. That last message I believe is in VS and not in MSBuild, and on that Drew may help.
I believe that setting is default for me, I checked and it was set. I'm seeing the same output you're seeing, but the behavior seems right. The output implies something different from what I'm seeing happening. @drewnoakes will be able to check the binlogs (and also, if you have the time I recommend them, they are an amazing tool :)). The quick check for you is: are you seeing the dll change at all (meaning, timestamps or hashes or something) in the case when the ts files are not changing and you still don't get 1 up-to-date in the log?

No .Net output file has been modified after compiling the application after modifying the ts file, as well as after compiling without modifications. The files are from last saturday when I made the last cs files changes.
This fits with the WARNING produced by Visual Studio up-to-date check.
1>FastUpToDate: Input TypeScriptCompile item 'C:\Users\olmob\source\repos\WebApplication1\WebApplication1\file.ts' is newer (2023-04-21 17:42:07.407) than earliest output 'C:\Users\olmob\source\repos\WebApplication1\WebApplication1\obj\Debug\net7.0\WebApplication1.pdb' (2023-04-15 22:26:56.965), not up-to-date. (WebApplication1)
1>FastUpToDate: Up-to-date check completed in 0,9 ms (WebApplication1)
1>------ Build started: Project: WebApplication1, Configuration: Debug Any CPU ------
1>WebApplication1 -> C:\Users\olmob\source\repos\WebApplication1\WebApplication1\bin\Debug\net7.0\WebApplication1.dll
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
========== Build started at 5:42 PM and took 00,492 seconds ==========
WARNING: Potential build performance issue in 'WebApplication1.csproj'. The project does not appear up-to-date after a successful build: Input TypeScriptCompile item 'C:\Users\olmob\source\repos\WebApplication1\WebApplication1\file.ts' is newer (2023-04-21 17:42:07.407) than earliest output 'C:\Users\olmob\source\repos\WebApplication1\WebApplication1\obj\Debug\net7.0\WebApplication1.pdb' (2023-04-15 22:26:56.965), not up-to-date. See https://aka.ms/incremental-build-failure.
I just realized that warning actually makes sense, but is absolutely irrelevant. The ts is newer than the pdb. What I'm not seeing in the logs is that item being marked as UpToDateCheckInput. The only things marked as input are some css files:

Checking the MSBuild Structured Log Viewer I can also see the CSS files... but also the cs files are not there and they are definitly an input of the compilation.
I think UpToDateCheckInput is only for adding some extra files
https://github.com/dotnet/project-system/blob/255712176d4b5dc4be054a45a5f63048aa89f4de/src/Microsoft.VisualStudio.ProjectSystem.Managed/ProjectSystem/DesignTimeTargets/Microsoft.Managed.DesignTime.targets#L414-L415
But there are some default rules going on that enlists all the cs files (correctly) and the ts files (incorrectly) in the default set.
@drewnoakes pointed to
<ProjectSchemaDefinitions xmlns="http://schemas.microsoft.com/build/2009/properties">
<ItemType Name="TypeScriptCompile" UpToDateCheckInput="True" />
</ProjectSchemaDefinitions>
No idea what is a ProjectSchemaDefinitions but looks promising to me.
Yeah, but that's not a thing I'm seeing anywhere on our stuff. I pinged drew on our internal systems but he's in Australia so already on a weekend. I'll have a chat with him and update here soonish :)
I found the issue :)
It seems the UpToDateCheckInput defaults to true which IMHO seems weird. The file is here: C:\Program Files\Microsoft Visual Studio\2022\[identifier]\MSBuild\Microsoft\VisualStudio\v17.0\TypeScript\ProjectItemsSchema.xaml (where "identifier" is usually Enterprise or Professional or Community). Try adding UpToDateCheckInput="false" to the ItemType declaration and see if that works for you.
If that works I still will have a chat with that team to understand, but it's a super simple fix for us.
Scratch that.. that has issues. We're close; we can investigate from here.
@joj what are the issues when using
<ItemType Name="TypeScriptCompile" DisplayName="TypeScript file" UpToDateCheckInput="false" />
?
@drewnoakes can Set="Scripts" be established by default for all the TypeScriptCompile?
The problem is that if you set typescript to not participate in up to date, then it doesn't... and changes to ts files don't end up triggering builds. We do participate in build, so that will leave stale code all over the place. To be clear: the warning that we're seeing is irrelevant, but it's not wrong: the ts file is newer than the binary output. The thing is that that is perfectly ok.
I do believe the real issue here is that we're mixing two completely different builds in a single project. It's like having two completely separate projects in the same project. It would make more sense to have the JavaScript part in a JavaScript Project (an esproj) and the C# part in a C# project. Then all the confusion will go away, because each one is in the right build and not interfering with each other.
The problem is that if you set typescript to not participate in up to date, then it doesn't... and changes to ts files don't end up triggering builds. We do participate in build, so that will leave stale code all over the place. To be clear: the warning that we're seeing is irrelevant, but it's not wrong: the ts file is newer than the binary output. The thing is that that is perfectly ok.
The warning is OK, the problem is that there is no way to solve it.
I do believe the real issue here is that we're mixing two completely different builds in a single project. It's like having two completely separate projects in the same project. It would make more sense to have the JavaScript part in a JavaScript Project (an esproj) and the C# part in a C# project. Then all the confusion will go away, because each one is in the right build and not interfering with each other.
I've tried this, but was not convinced with splitting in csproj and esproj projects, let me explain why:
<RANT_START>
We've been building a framework for writing Line of Business applications since... 15 years or so. We have many applications with it and extracted like 40 reusable modules, some are use in almost any project (like 25-30) while the others are used only sometimes.
By a module, I mean a vertical module that contains entities, logic, queries, etc (in CS) and UI and client side code (TS/TSX).
One project per layer (Entities / Logic / React)
Until now the code was structured by layers:
- Signum.Entities.Extensions.csproj contains all the entities, each module in a folder / namespace.
- Signum.Engine.Extensions.csproj contains all the business logic, each module in a folder / namespace.
- Signum.React.Extensions.csproj contains all the controllers (cs) and Client and UI code in TS, each module in a folder / namespace.
The reason for dividing the projects per layer and not per module is mainly a compilation performance issue.
One project per layers and modules
This architecture is now making problems. We have tried diving it by 'groups of modules' but that doesn't solve anything really. The only natural division is each module in a different project... this means 40 x 3 => 120 projects. Very slow to compile.
One project per language and module
Another alternative was merging Entities/Logic/Controller into one csproj and all the typescript code into a esproj, that was the status 3 weeks ago. But this means 80 projects many with just a few files. Every dependency between modules means references in csproj, in esproj and in tsconfig.
The compilation was quite slow and navigating the project structure was inconvinient. Also the esproj projects are still in alpha.
One project per module
Finally the current organization, and the one we are more happy with, is to put all the code of a module in one just one project. This means only having 40 projects in Extensions. Take into account that some applications also contains internal modules, so the number could be higher (about 80)
This has the nice benefit that all the related code is together, doesn't matter if is backend code or frontend code. For the type of code that we write this is very convinient, but had many technical challenges.
One of this is compilation time. Somehow VS calling TSC 40 times, one for each project, is much slower than calling tsc --build manually at the end in the final application and letting it compile all the projects in one call. (From 7mins to 2mins!!)
Another problem is that NPM Task Runner didn't support Yarn Workspaces, but this is solved now.
And the remaining issue is this one: A change in any ts/tsx file makes the csproj never be cached as compiled again until a change in a cs project is made.
Sorry for the long rant. I know that most of the are my problems but I'm sure that changes in this direction will benefit other teams that build hybrid CS / TS applications and want to stay in Visual Studio.
<RANT_END>
So I already have the compilation split anyway, by:
- Adding
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>in Directory.Build.props - Running
tsc --buildas aPostBuildEventto get TS errors. - Using
yarn/webpackfor generating javascript and bundling the application.
I only really need VS for the IntelliSense...
Is there a way to have IntelliSense in TS/TSX files without triggering a compilation if a file changes?
Right now TypeScriptCompileBlocked doesn't solve the issue. And <TypeScriptCompile Remove="MyFile.tsx" /> completly removes the project from the solution.
@drewnoakes maybe some creative ideas from the VS side?
I don't want to deviate too much, but I do have a gut reaction: does it make sense for you to turn some or most of those modules nuget packages (when C#) or npm modules (when ts/js)? Specially with js/ts that will make them easier to use in your LOB apps while not losing the ability to debug them.
We tried something like this for a month in.... 2008. The amount of changes that we do in the framework / extensions is too high (24000 commits) to trigger a release, wait for CI, update many Nuget and NPM dependencies etc..
Also the code in Extensiosn is very similar to the the code that you write in the application itself, so a white box approach makes more sense than a black box approach.
Finally, once about 70-80% of the application is a reusable library, users of the framework need the ability to change (maybe a temporal hack) and release quick, and then discuss whether the was a good solution.
All this make us decide to distribute the application as Git SubModules instead of NPM / Nuget.
Back to the topic, it is true that we suffer this problem more than simpler/more standard applications, but the point of the nuget Microsoft.Typescript.MSBuild, with 14.6M downloads, is to enable TypeScript development inside .Net projects and the current behaviour is broken.
I have some alternatives that maybe work:
- Is there a way to force a .Net compilation even if there are no CS changes? Including this in
Microsoft.TypeScript.MSBuild. - @drewnoakes it is/could be possible to add a
Setargument to<ItemType Name="TypeScriptCompile" UpToDateCheckInput="True" />? - Is is possible to make
UpToDateCheckInput="True"depening onTypeScriptCompileBlocked
I had a talk with Drew yesterday and we believe this can indeed be fixed using sets. I'm going to give this issue to someone in my team or fix it myself. I'll update once we have more information.
I've found a solution!
<PropertyGroup>
<BuildDependsOn>
FixProjectLastWriteTime;
$(BuildDependsOn);
</BuildDependsOn>
</PropertyGroup>
<Target Name="FixProjectLastWriteTime">
<SetProjectLastWriteTime FileList="@(TypeScriptCompile)" ProjectFile="$(MSBuildProjectFile)"/>
</Target>
<UsingTask TaskName="SetProjectLastWriteTime"
TaskFactory="RoslynCodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll" >
<ParameterGroup>
<FileList ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
<ProjectFile ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System" />
<Code Type="Fragment" Language="cs">
<![CDATA[
var maxDate = DateTime.MinValue;
foreach(var file in FileList)
{
var date = DateTime.Parse(file.GetMetadata("ModifiedTime"));
if(date > maxDate)
{
maxDate = date;
}
}
DateTime cacheTimeStamp = File.GetLastWriteTime(ProjectFile);
if(maxDate > cacheTimeStamp)
{
File.SetLastWriteTime(ProjectFile, maxDate);
Log.LogMessage(MessageImportance.High, "Changing LastWriteTime of {0} to {1} (latest TypeScriptCompile)", ProjectFile, maxDate);
}
]]>
</Code>
</Task>
</UsingTask>
Before I tried something simpler, using the TouchTask agains $(MSBuildProjectFile), but didnt't work because InputModifiedSinceLastSuccessfulBuildStart.
Fortunately I found some inspiration in this answer.
What you think? The solution is a little bit hacky, but doesn't change the file history in GIT and is simple.
FWIW, I tried the following targets to fix up the TypeScript up-to-date-check:
<Target Name="FixUpToDateCheckInput" BeforeTargets="CollectUpToDateCheckInputDesignTime">
<ItemGroup>
<UpToDateCheckInput>
<Set Condition="$([MSBuild]::ValueOrDefault('%(Identity)', '').EndsWith('.ts'))">TypeScript;%(Set)</Set>
</UpToDateCheckInput>
</ItemGroup>
</Target>
<Target Name="FixUpToDateCheckOutput" BeforeTargets="CollectUpToDateCheckOutputDesignTime">
<ItemGroup>
<UpToDateCheckOutput>
<Set Condition="'%(Identity)' == 'wwwroot\index.html'">TypeScript;%(Set)</Set>
</UpToDateCheckOutput>
</ItemGroup>
</Target>
Debugging the design-time build does show the set gets applied but it seems to be to no avail as the files are still added separately to the default set via TypeScriptCompile inputs.
Yes, you'd need to override CollectUpToDateCheckInputDesignTime to exclude the TypeScriptCompile items somehow.
@drewnoakes I tried that already with something like:
<Target Name="FixUpToDateCheckInput" BeforeTargets="CollectUpToDateCheckInputDesignTime" Condition="'$(BuildingInsideVisualStudio)' == 'true'">
<ItemGroup>
<UpToDateCheckInput Remove="@(TypeScriptCompile)" />
</ItemGroup>
</Target>
Debugging the design-time build does show the inputs get removed but this has no effect.
I've concluded the only way to fix this is for TypeScriptCompile item types to be declared via ProjectSchemaDefinition or otherwise such that they register their own group/set with up-to-date-check, as you mention in https://github.com/microsoft/TypeScript/issues/53795#issuecomment-1517035277.
Do you have mapping data for input-to-output files? If so, you can use UpToDateCheckBuilt items and specify the Original metadata.
Conceptually you need:
<ItemGroup>
<UpToDateCheckBuilt Include="Destination\MyFile.js" Original="Source\MyFile.ts" />
</ItemGroup>
With this, the FUTDC will compare the timestamp of the .js file with the corresponding .ts file.