dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.21k stars 9.95k forks source link

AngularCliMiddleware is not working with the new Angular CLI's build system #53307

Open PeteAtWSA opened 8 months ago

PeteAtWSA commented 8 months ago

Is there an existing issue for this?

Describe the bug

Serving an Angular 17 SPA build with the new Angular CLI's build system. Angular.io guide for esbuild

UseAngularCliServer(npmScript: start) is running into a timeout.

For example, consider the following code in Startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  app.UseSpa(
      spa =>
      {
          spa.Options.SourcePath = "ClientApp";app.UseSpa(
      spa =>
      {
          spa.Options.SourcePath = "ClientApp";
          spa.UseAngularCliServer("start");
      });
          spa.UseAngularCliServer("start");
      });
}

After a timeout the following error is thrown: "The Angular CLI process did not start listening for requests within the timeout period of 120 seconds. Check the log output for error information."

So it does not support the new Angular build system.

Expected Behavior

The ASP .NET core Backend and the Angular SPI is served.

Steps To Reproduce

Create a new "Angular and ASP.NET Core" project from Visual Studio 2022 project templates. Change the angular.json of the newly created project as following: Using the browser-esbuild builder

The following is what you would typically find in angular.json for an application:

"architect": {
  "build": {
    "builder": "@angular-devkit/build-angular:browser",

Changing the builder field is the only change you will need to make.

"architect": {
  "build": {
    "builder": "@angular-devkit/build-angular:browser-esbuild",

Exceptions (if any)

TimeoutException: The Angular CLI process did not start listening for requests within the timeout period of 120 seconds. Check the log output for error information. Microsoft.AspNetCore.SpaServices.Extensions.Util.TaskTimeoutExtensions.WithTimeout(Task task, TimeSpan timeoutDelay, string message) Microsoft.AspNetCore.SpaServices.Extensions.Proxy.SpaProxy.PerformProxyRequest(HttpContext context, HttpClient httpClient, Task baseUriTask, CancellationToken applicationStoppingToken, bool proxy404s) Microsoft.AspNetCore.Builder.SpaProxyingExtensions+<>c__DisplayClass2_0+<b__0>d.MoveNext() Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

.NET Version

8.0.101

Anything else?

In Microsoft.AspNetCore.SpaServices.AngularCli.AngularCliMiddleware.cs is a condition for waiting until the output of the ng serve command is matching a specific regex pattern:

source code

openBrowserLine = await scriptRunner.StdOut.WaitForMatch(
    new Regex("open your browser on (http\\S+)", RegexOptions.None, RegexMatchTimeout));

But the output has changed due to the new Angular build system.

Old build system:

2024-01-11_16h23_02

New build system:

2024-01-11_16h23_28

So the regex pattern does not match any more. This pattern has to be fitted!

PeteAtWSA commented 8 months ago

my suggestion is to change the regex pattern to "(open your browser on (http\S+)|Local: http\S+)"

pchriste24 commented 6 months ago

Is there any plan to fix this or to allow the use of a custom regex as suggested in this #52325 issue?

SoyDiego commented 6 months ago

Hi, I have the same problem. I changed to the new build system application and now I receive the timeout after 120 seconds. I tried a lot of things but I couldn't. And I didn't find updated posts talking about this problem.

Any help or anyone got a solution?

Thanks!

SoyDiego commented 6 months ago

@PeteAtWSA did you find another solution? Please if you have any help, I appreaciate

PeteAtWSA commented 6 months ago

We are printing the expected text before the serve command:

"start": "echo open your browser on https://localhost:4200/ && ng serve"

SoyDiego commented 6 months ago

We are printing the expected text before the serve command:

"start": "echo open your browser on https://localhost:4200/ && ng serve"

Thanks again, I added to to my package.json

 "start": "echo 'open your browser on https://localhost:4200/' && ng serve",

But I continue failing in the timeout. Any idea why is continue failing?

image

Thanks again @PeteAtWSA

SoyDiego commented 6 months ago

Finally I solved my problem. I don't know if the best solution and my knowledge in .NET is 0. I had this code before I changed the build system:

app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "../../MyProject";

        if (environment.IsDevelopment())
        {
            spa.UseAngularCliServer(npmScript: "start");
        }

        spa.Options.DefaultPageStaticFileOptions = new StaticFileOptions
        {
            OnPrepareResponse = ctx =>
            {
                var headers = ctx.Context.Response.GetTypedHeaders();
                headers.CacheControl = new CacheControlHeaderValue
                {
                    NoCache = true,
                    NoStore = true,
                    MustRevalidate = true,
                    MaxAge = TimeSpan.Zero
                };
            }
        };
    });

And I realized that if I run my frontend and backend separately (by executing npm start from my frontend), everything was working perfectly. So I was researching how to execute my process directly from .NET without spa.UseAngularCliServer(npmScript: "start") and I did using ProcessStartInfo.

The final code looks like this:

app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "../../MyProject";

        if (environment.IsDevelopment())
        {
            string angularProjectPath = spa.Options.SourcePath;

            ProcessStartInfo psi = new ProcessStartInfo
            {
                FileName = "npm",
                Arguments = "start",
                WorkingDirectory = angularProjectPath,
                UseShellExecute = true,
            };

            Process process = Process.Start(psi);

            spa.UseProxyToSpaDevelopmentServer("http://localhost:4200");
        }
    });

This code works perfectly, and I didn't need to use any other possible solutions involving modified NuGet packages.

I hope the solution helps others with the same problem!

awdorrin commented 1 week ago

I've found AspNetCore.SpaYarp by Bernd Hirschmann, and after only a few days of experimenting with it, it seems like a better approach than both the legacy and new SPA Proxy methods available with the old and new templates. Moving the YARP proxy to the back-end fixes CORS issues when using back-end authentication middleware, and his approach to monitoring for the start of the SPA side seems much cleaner.