JavaScriptServices cannot host multiple SPAs

Created on 24 Jan 2018  路  19Comments  路  Source: aspnet/JavaScriptServices

Functional impact

I am attempting to host multiple SPA's by making multiple calls to app.UseSpa, but whichever SPA is registered first intercepts all incoming requests.

Minimal repro steps

These steps will use Angular with Angular's i18n tools to generate multiple apps, but any scenario with multiple apps should produce the same results.

  1. install Microsoft.DotNet.Web.Spa.ProjectTemplates::2.0.0-rc1-final
  2. create a new app: dotnet new angular
  3. navigate to ClientApp
  4. run npm install
  5. Create a second SPA. I don't think it matters how you do this, I found the issue by using Angular's i18n tools to compile multiple localized versions of the app, but for simplicity, I duplicated it by copying ./ClientApp to ./ClientApp2, and adjusting the build/serve scripts in the package.json files to deploy to different paths:

    1. ./ClientApp/package.json scripts:
      json "start": "ng serve --extract-css --deploy-url=/app1/ --base-href=/app1/", "build": "ng build --extract-css --deploy-url=/app1/ --base-href=/app1/",
    2. ./ClientApp2/package.json scripts:
      json "start": "ng serve --extract-css --deploy-url=/app2/ --base-href=/app2/", "build": "ng build --extract-css --deploy-url=/app2/ --base-href=/app2/",
    3. ./ClientApp2/src/app/app.component.html add an identifier so we can differentiate the apps:
      html <div class='container-fluid'> <div class='row'> <div class='col-sm-3'> <app-nav-menu></app-nav-menu> </div> <div class='col-sm-9 body-content'> <h2>App 2</h2> <router-outlet></router-outlet> </div> </div> </div>
    4. ./Startup.cs configure two SPAs:
      ```C#
      app.UseSpa(spa =>
      {
      // To learn more about options for serving an Angular SPA from ASP.NET Core,
      // see https://go.microsoft.com/fwlink/?linkid=864501

      spa.Options.SourcePath = "ClientApp";
      spa.Options.DefaultPage = "/app1/index.html";
      
      if (env.IsDevelopment())
      {
          spa.UseAngularCliServer(npmScript: "start");
      }
      

      });

      app.UseSpa(spa =>
      {
      // To learn more about options for serving an Angular SPA from ASP.NET Core,
      // see https://go.microsoft.com/fwlink/?linkid=864501

      spa.Options.SourcePath = "ClientApp2";
      spa.Options.DefaultPage = "/app2/index.html";
      
      if (env.IsDevelopment())
      {
          spa.UseAngularCliServer(npmScript: "start");
      }
      

      });
      ```

  6. Start the server with dotnet run
  7. Navigate to http://localhost:{port}/app1; app1 loads as expected
  8. Navigate to http://localhost:{port}/app2; app1 is loaded, and you are redirected to /app1

If we disable the first call to app.UseSpa or reverse the order, app2 loads instead of app1.

Expected result

Navigating to the base path of an SPA should route to that specific SPA.

Actual result

Whichever SPA is configured first handles all requests.

Further technical details

While I am a newcomer to .Net Core MVC/Javascript services, from what I can tell so far, it looks like the SPA configuration needs an option to specify a route to handle requests for.

Microsoft.AspNetCore.SpaServices.SpaDefaultPageMiddleware.Attach rewrites all requests to point to options.DefaultPage, which is then either handled by the middleware attached by app.UseSpaStaticFilesInternal or caught by an error handler. In my example, a request coming in to /app2 gets pointed to /app1/index.html, which is served up by the static file middleware for app1, and so never gets to the middleware configured by the second call to app.UseSpa.

Overriding the default file provider (as suggested in code comments) doesn't help, because the request path is still being overwritten, so we can't distinguish the apps at the file provider. Even if we could, the request would be caught by the error handler before it could get to the middleware configured for the second SPA.

The path rewrite and the error handler need to be aware of the SPA base path, so requests can pass to the appropriate apps. Perhaps something akin to the way routing is handled in .Net Core MVC?

Most helpful comment

Heres my setup:

services.AddSpaStaticFiles(configuration =>
{
    configuration.RootPath = "ClientApp/dist/apps";
});
app.Map("/app1", appSearch => appSearch.UseSpa(spa =>
{
    spa.Options.SourcePath = "ClientApp";
    spa.Options.DefaultPage = "/app1/index.html";

    if (env.IsDevelopment())
    {
        spa.UseAngularCliServer(npmScript: "start -- --app=app1 --base-href=/app1/ --serve-path=/");
    }
}));

app.Map("/app2", appSearch => appSearch.UseSpa(spa =>
{
    spa.Options.SourcePath = "ClientApp";
    spa.Options.DefaultPage = "/app2/index.html";

    if (env.IsDevelopment())
    {
        spa.UseAngularCliServer(npmScript: "start -- --app=app2 --base-href=/app2/ --serve-path=/");
    }
}));

When in development I have successfully run two SPA in individual app.Map callback without issues.

Production appears to working as well. I use the following commands to generate published versions:

  • Running npm run build --app=app1 --base-href=/app1/, output is sent to ClientApp/dist/apps/app1
  • Running npm run build --app=app2 --base-href=/app2/, output is sent to ClientApp/dist/apps/app2

The only issue I am aware of is when under development, in the browser network tab I see a lot of 404 errors for http://localhost:60142/sockjs-node/info?t=XXXXXXXXXXXX. I assume this is because these should be translated into http://localhost:60142/app1/sockjs-node/info?t=XXXXXXXXXXXX and http://localhost:60142/app2/sockjs-node/info?t=XXXXXXXXXXXX. I not sure if this is a Angular CLI configuration error or something to do with the middleware setup. @SteveSandersonMS any ideas on how to resolve this?

All 19 comments

For this to work, you will need to branch the ASP.NET middleware chain so that the correct requests get routed to each of the UseSpa middlewares. In your code sample, both UseSpa middlewares are in the same branch so one of them will take precedence and handle all the requests. Please see https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware?tabs=aspnetcore2x#use-run-and-map, and put the two UseSpa calls in two different app.Map callbacks that handle non-overlapping portions of the URL space (e.g., /myapp1 and /myapp2).

Heres my setup:

services.AddSpaStaticFiles(configuration =>
{
    configuration.RootPath = "ClientApp/dist/apps";
});
app.Map("/app1", appSearch => appSearch.UseSpa(spa =>
{
    spa.Options.SourcePath = "ClientApp";
    spa.Options.DefaultPage = "/app1/index.html";

    if (env.IsDevelopment())
    {
        spa.UseAngularCliServer(npmScript: "start -- --app=app1 --base-href=/app1/ --serve-path=/");
    }
}));

app.Map("/app2", appSearch => appSearch.UseSpa(spa =>
{
    spa.Options.SourcePath = "ClientApp";
    spa.Options.DefaultPage = "/app2/index.html";

    if (env.IsDevelopment())
    {
        spa.UseAngularCliServer(npmScript: "start -- --app=app2 --base-href=/app2/ --serve-path=/");
    }
}));

When in development I have successfully run two SPA in individual app.Map callback without issues.

Production appears to working as well. I use the following commands to generate published versions:

  • Running npm run build --app=app1 --base-href=/app1/, output is sent to ClientApp/dist/apps/app1
  • Running npm run build --app=app2 --base-href=/app2/, output is sent to ClientApp/dist/apps/app2

The only issue I am aware of is when under development, in the browser network tab I see a lot of 404 errors for http://localhost:60142/sockjs-node/info?t=XXXXXXXXXXXX. I assume this is because these should be translated into http://localhost:60142/app1/sockjs-node/info?t=XXXXXXXXXXXX and http://localhost:60142/app2/sockjs-node/info?t=XXXXXXXXXXXX. I not sure if this is a Angular CLI configuration error or something to do with the middleware setup. @SteveSandersonMS any ideas on how to resolve this?

Thank you @SteveSanderson, @chris5287! Between the two of you, I got my apps running the way I want.

@asgallant great news, did you get the live reload to work? (seee my sockjs issues above),

I made my own go at this, trying to POC a requirement of running independent Angular apps in the same host. Results are here:
https://github.com/nickwesselman/SpaServicesMultiSpa

The hard work:
https://github.com/nickwesselman/SpaServicesMultiSpa/blob/master/spa-services/SpaPluginExtensions.cs

Though it works, I think this could definitely be easier:

  • Exposing DefaultSpaStaticFileProvider or similar, so that each app can configure its own file provider, would be helpful since it gracefully handles the path not existing when in development/proxy mode. As it was I needed to add some exception logic around calling UsaSpaStaticFiles.
  • Would be nice to have an easier way to share app-specific file providers between UseSpaStaticFiles and SpaOptions.DefaultPageStaticFileOptions. Wasn't obvious at first that I needed to populate that file provider in both locations.
  • Some docs on how to get live reload working when mapping the SPA at a different path would be helpful.
  • Debugging SSR is tricky too with multiple node instances. (Plus using AddNodeServices to specify debug options works with one app, but then causes the second app to use the same node instance, making Angular angry). I ended up creating my own INodeServices registration with incrementing debug ports (https://github.com/nickwesselman/SpaServicesMultiSpa/blob/master/spa-services/DebugNodeServicesExtensions.cs). Perhaps the NodeServicesOptions could be exposed on SpaPrerenderingOptions?

@SteveSandersonMS any of the above make it worth re-opening this, or should I log in new issue(s)?

@nickwesselman Since this is a relatively uncommon setup, we'd probably not change the APIs too much based on it, though if many more people report having trouble with it that might be reviewed. Regarding docs, would you be interested in submitting a short PR to the docs repo about live reloading?

A fix for the live reloading would be appreciated!

@chris5287 Check my repo above, it has it working. The trick w/ multiple apps is to set the live-reload-client argument on ng serve:
https://github.com/nickwesselman/SpaServicesMultiSpa/blob/master/spa-services/ClientApp/package.json

You might also need to update the ExcludeUrls for the mapped path if you are using pre-rendering as well.

@SteveSandersonMS I'd be happy to contribute a doc update on this, will do so later this week.

@nickwesselman is there a way to fix live reloading without hardcoding the host/port into package.json?

@chris5287 Unfortunately live-reload-client (which is an alias for public-host) requires a full URL, and UseAngularCliServer doesn't provide a means of passing arguments to the npm script, so it needs to be placed in the package.json. That's why I created a separate script for running in SpaServices. Might look at a PR to allow UseAngularCliServer to pass args? This would at least allow the URL to be constructed dynamically.

I'm trying desperately to get this working for a clients app im trying to deploy.. I've developed them in different projects / folders completely and am " ng build " 'ing them into ClientApp, AdminApp inside my .net core route directory.

I used the code above, but cant get it working. I don't want or need to run them in any circumstance within the same project, Im happy developing them in separate projects, I'm just trying to get these deployed urgently.

Any advice would be GREATLY appreciate. I'm quite surprised there isnt more documentation on how to do such a common task.

Cheers

this is my current error. Im guessing because it's trying to read NPM in the published static files, which isnt there.

`An unhandled exception occurred while processing the request.
AggregateException: One or more errors occurred. (One or more errors occurred. (Failed to start 'npm'. To resolve this:.

[1] Ensure that 'npm' is installed and can be found in one of the PATH directories.
Current PATH enviroment variable is: C:\Program Files\ConEmu\ConEmu\Scripts;C:\Program Files\ConEmu;C:\Program Files\ConEmu\ConEmu;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\Program Files\Docker\Docker\Resources\bin;C:\ProgramData\Oracle\Java\javapath;C:\Program Files (x86)\Razer Chroma SDK\bin;C:\Program Files\Razer Chroma SDK\bin;C:\Program Files (x86)\Common Files\Intel\Shared Libraries\redist\ia32\compiler;C:\Program Files (x86)\Common Files\Intel\Shared Libraries\redist\intel64\compiler;C:\Windowssystem32;C:\Windows;C:\WindowsSystem32\Wbem;C:\WindowsSystem32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files (x86)\Microsoft SQL Server\Client SDK\ODBC\130\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\140\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\140\DTS\Binn\;C:\Program Files (x86)\Microsoft SQL Server\140\Tools\Binn\ManagementStudio\;C:\Program Files\dotnet\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\WINDOWSsystem32;C:\WINDOWS;C:\WINDOWSSystem32\Wbem;C:\WINDOWSSystem32\WindowsPowerShell\v1.0\;C:\Program Files\TortoiseGit\bin;C:\Program Files\Microsoft VS Code\bin;C:\Users\willi\AppData\Local\Microsoft\WindowsApps;C:\Users\willi\AppData\Roamingnpm;C:\Program Files\Git\cmd;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\usr\bin;C:\Program Files\PostgreSQL\10\bin;C:\Program Files\PostgreSQL\10\lib;C:\Program Files\Microsoft VS Code;C:\Program Files\PuTTY\;C:\ProgramData\chocolatey\bin;C:\Program Files (x86)\Windows Kits\8.1\Windows Performance Toolkit\;C:\Program Files\nodejs\;C:\Program Files\MySQL\MySQL Server 8.0\bin;C:\WINDOWSsystem32;C:\WINDOWS;C:\WINDOWSSystem32\Wbem;C:\WINDOWSSystem32\WindowsPowerShell\v1.0\;C:\WINDOWSSystem32\OpenSSH\;C:\Program Files\Microsoft VS Code_;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\Program Files\Docker\Docker\Resources\bin;C:\ProgramData\Oracle\Java\javapath;C:\Program Files (x86)\Razer Chroma SDK\bin;C:\Program Files\Razer Chroma SDK\bin;C:\Program Files (x86)\Common Files\Intel\Shared Libraries\redist\ia32\compiler;C:\Program Files (x86)\Common Files\Intel\Shared Libraries\redist\intel64\compiler;C:\Windowssystem32;C:\Windows;C:\WindowsSystem32\Wbem;C:\WindowsSystem32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files (x86)\Microsoft SQL Server\Client SDK\ODBC\130\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\140\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\140\DTS\Binn\;C:\Program Files (x86)\Microsoft SQL Server\140\Tools\Binn\ManagementStudio\;C:\Program Files\dotnet\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\WINDOWSsystem32;C:\WINDOWS;C:\WINDOWSSystem32\Wbem;C:\WINDOWSSystem32\WindowsPowerShell\v1.0\;C:\Program Files\TortoiseGit\bin;C:\Program Files\Microsoft VS Code\bin;C:\Users\willi\AppData\Local\Microsoft\WindowsApps;C:\Users\willi\AppData\Roamingnpm;C:\Program Files\Git\cmd;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\usr\bin;C:\Program Files\PostgreSQL\10\bin;C:\Program Files\PostgreSQL\10\lib;C:\Program Files\Microsoft VS Code;C:\Program Files\PuTTY\;C:\ProgramData\chocolatey\bin;C:\Program Files (x86)\Windows Kits\8.1\Windows Performance Toolkit\;C:\Program Files\nodejs\;C:\Program Files\MySQL\MySQL Server 8.0\bin;C:\WINDOWSsystem32;C:\WINDOWS;C:\WINDOWSSystem32\Wbem;C:\WINDOWSSystem32\WindowsPowerShell\v1.0\;C:\WINDOWSSystem32\OpenSSH\;C:\Program Files\Microsoft VS Code_;;C:\Program Files\Microsoft VS Code_
Make sure the executable is in one of those directories, or update your PATH.

[2] See the InnerException for further details of the cause.))
System.Threading.Tasks.Task.GetResultCore(bool waitCompletionNotification)

InvalidOperationException: Failed to start 'npm'. To resolve this:.

[1] Ensure that 'npm' is installed and can be found in one of the PATH directories.
Current PATH enviroment variable is: C:\Program Files\ConEmu\ConEmu\Scripts;C:\Program Files\ConEmu;C:\Program Files\ConEmu\ConEmu;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\Program Files\Docker\Docker\Resources\bin;C:\ProgramData\Oracle\Java\javapath;C:\Program Files (x86)\Razer Chroma SDK\bin;C:\Program Files\Razer Chroma SDK\bin;C:\Program Files (x86)\Common Files\Intel\Shared Libraries\redist\ia32\compiler;C:\Program Files (x86)\Common Files\Intel\Shared Libraries\redist\intel64\compiler;C:\Windowssystem32;C:\Windows;C:\WindowsSystem32\Wbem;C:\WindowsSystem32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files (x86)\Microsoft SQL Server\Client SDK\ODBC\130\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\140\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\140\DTS\Binn\;C:\Program Files (x86)\Microsoft SQL Server\140\Tools\Binn\ManagementStudio\;C:\Program Files\dotnet\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\WINDOWSsystem32;C:\WINDOWS;C:\WINDOWSSystem32\Wbem;C:\WINDOWSSystem32\WindowsPowerShell\v1.0\;C:\Program Files\TortoiseGit\bin;C:\Program Files\Microsoft VS Code\bin;C:\Users\willi\AppData\Local\Microsoft\WindowsApps;C:\Users\willi\AppData\Roamingnpm;C:\Program Files\Git\cmd;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\usr\bin;C:\Program Files\PostgreSQL\10\bin;C:\Program Files\PostgreSQL\10\lib;C:\Program Files\Microsoft VS Code;C:\Program Files\PuTTY\;C:\ProgramData\chocolatey\bin;C:\Program Files (x86)\Windows Kits\8.1\Windows Performance Toolkit\;C:\Program Files\nodejs\;C:\Program Files\MySQL\MySQL Server 8.0\bin;C:\WINDOWSsystem32;C:\WINDOWS;C:\WINDOWSSystem32\Wbem;C:\WINDOWSSystem32\WindowsPowerShell\v1.0\;C:\WINDOWSSystem32\OpenSSH\;C:\Program Files\Microsoft VS Code_;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\Program Files\Docker\Docker\Resources\bin;C:\ProgramData\Oracle\Java\javapath;C:\Program Files (x86)\Razer Chroma SDK\bin;C:\Program Files\Razer Chroma SDK\bin;C:\Program Files (x86)\Common Files\Intel\Shared Libraries\redist\ia32\compiler;C:\Program Files (x86)\Common Files\Intel\Shared Libraries\redist\intel64\compiler;C:\Windowssystem32;C:\Windows;C:\WindowsSystem32\Wbem;C:\WindowsSystem32\WindowsPowerShell\v1.0\;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files (x86)\Microsoft SQL Server\Client SDK\ODBC\130\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\140\Tools\Binn\;C:\Program Files (x86)\Microsoft SQL Server\140\DTS\Binn\;C:\Program Files (x86)\Microsoft SQL Server\140\Tools\Binn\ManagementStudio\;C:\Program Files\dotnet\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\WINDOWSsystem32;C:\WINDOWS;C:\WINDOWSSystem32\Wbem;C:\WINDOWSSystem32\WindowsPowerShell\v1.0\;C:\Program Files\TortoiseGit\bin;C:\Program Files\Microsoft VS Code\bin;C:\Users\willi\AppData\Local\Microsoft\WindowsApps;C:\Users\willi\AppData\Roamingnpm;C:\Program Files\Git\cmd;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\usr\bin;C:\Program Files\PostgreSQL\10\bin;C:\Program Files\PostgreSQL\10\lib;C:\Program Files\Microsoft VS Code;C:\Program Files\PuTTY\;C:\ProgramData\chocolatey\bin;C:\Program Files (x86)\Windows Kits\8.1\Windows Performance Toolkit\;C:\Program Files\nodejs\;C:\Program Files\MySQL\MySQL Server 8.0\bin;C:\WINDOWSsystem32;C:\WINDOWS;C:\WINDOWSSystem32\Wbem;C:\WINDOWSSystem32\WindowsPowerShell\v1.0\;C:\WINDOWSSystem32\OpenSSH\;C:\Program Files\Microsoft VS Code_;;C:\Program Files\Microsoft VS Code_
Make sure the executable is in one of those directories, or update your PATH.`

if I comment out the
if (env.IsDevelopment()) { // this defaults to start:hosted, which has some ng serve options for multi-spa // spa.UseAngularCliServer(npmScript: options.DevServerScript); }

I get
An unhandled exception occurred while processing the request. NodeInvocationException: Prerendering failed because of error: Error: Cannot find module 'D:\Development\Projects\Crypto\Zest-Masternode-Monitor\ZestMonitor.Api\ClientApp\dist-server\main.bundle.js' at Function.Module._resolveFilename (module.js:547:15) at Function.Module._load (module.js:474:25) at Module.require (module.js:596:17) at require (internal/module.js:11:18) at findBootModule (C:\Users\willi\AppData\Local\Temp\xlw3jsqg.0gh:111:17) at findRenderToStringFunc (C:\Users\willi\AppData\Local\Temp\xlw3jsqg.0gh:116:28) at renderToStringImpl (C:\Users\willi\AppData\Local\Temp\xlw3jsqg.0gh:75:51) at C:\Users\willi\AppData\Local\Temp\rl24f40m.0zd:114:19 at IncomingMessage.<anonymous> (C:\Users\willi\AppData\Local\Temp\rl24f40m.0zd:133:38) at emitNone (events.js:106:13) Current directory is: D:\Development\Projects\Crypto\Zest-Masternode-Monitor\ZestMonitor.Api Microsoft.AspNetCore.NodeServices.HostingModels.HttpNodeInstance.InvokeExportAsync<T>(NodeInvocationInfo invocationInfo, CancellationToken cancellationToken)

Alright.. so I removed pre-rendering
// spa.UseSpaPrerendering(renderingOptions => // { // renderingOptions.BootModulePath = $"{spa.Options.SourcePath}{options.ServerRenderBundlePath}"; // renderingOptions.BootModuleBuilder = env.IsDevelopment() // ? new AngularCliBuilder(npmScript: options.ServerRenderBuildScript) // : null; // // proxied socksjs-node will be under the branched path // renderingOptions.ExcludeUrls = new[] { $"{options.MapPath}/sockjs-node" }; // });
But now I dont get returned any data in the frontend.

@digitlaninja I haven't looked at this since I put together the POC linked above and my memory of how it even works has faded quite a bit, but it looks like you are using my POC (based on use of start:hosted). Without dev mode or prerendering I'm not sure why you'd use SpaServices. Your first post/error just looks like you need node/npm installed and in your PATH. That's about all the help I can offer.

@nickwesselman thanks so much for your response man.
So the build/deploy pipeline im using is:
I have 3 app projects.
1x Angular Client
1x Angular Admin (cms)
1x dotnet Core api.
I'm deving in 3 different text editors and they are completely separate, both just calling the API.

I'm using ng build --prod' to build into my www/root of the api.
But I obviously can't just build both in there.
I'm following a tutorial in a course and he does it this way, I have no idea how I can simply get the 2 UI's published so I can then deploy them to my VPS. I've scoured the net and spent the entire weekend looking.. Any help would be hugely appreciated, I'm stuck.

Like I said, I provided the POC above as-is and is all the help I can offer:
https://github.com/nickwesselman/SpaServicesMultiSpa

It worked great last I ran it, but I have not touched this stuff since I posted it.

Thank you so much nickwesselman for multi-spa solution. My problem was mostly when I was attempting to put my app to production. Having a PhysicalFileProvider for each spa solved this issue. It's hard to find solutions like this.
+1

Was this page helpful?
0 / 5 - 0 ratings