Eptagone / Vite.AspNetCore

Small library to integrate Vite into ASP.NET projects
MIT License
264 stars 35 forks source link

Tag helper vite-src not behaving as expected when switching ASPNETCORE_ENVIRONMENT from "Development" to "Production" #115

Closed BernardHymmen closed 6 months ago

BernardHymmen commented 6 months ago

I'm migrating an existing site from Webpack to Vite so that I can use a Razor page to do some necessary server-side processing and I'm using Vite.AspNetCore to do it. My _Layout.cshtml page (edited down for brevity) looks like this:

@addTagHelper *, Vite.AspNetCore
@inject IViteManifest Manifest

<head>
    <link rel="icon" type="image/x-icon" href="/images/favicon-light.ico">
    @await RenderSectionAsync("HeadEntries", required: false)
    <script defer type="module" vite-src="~/ThemedFavicon.ts" asp-append-version="true"></script>
</head>
<body>
    <div id="root"></div>
</body>

and the relevant part of my index.cshtml page looks like this:

@page
@addTagHelper *, Vite.AspNetCore
@inject IViteManifest Manifest

@section HeadEntries {
     <!-- From index.cshtml vvvv -->
     <!-- ASPNETCORE_ENVIRONMENT = "@(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "<null>")" -->
     <link rel="stylesheet" href="/css/main.css">
    <script type="module" vite-src="~/main.tsx" asp-append-version="true"></script>
    <!-- From index.cshtml ^^^^ -->
}

When I run this with ASPNECTCORE_ENVIRONMENT set to "Development" I get this rendered HTML output, and my site works as expected.

<head>
    <link rel="icon" type="image/x-icon" href="/images/favicon-light.ico">
     <!-- From index.cshtml vvvv -->
     <!-- ASPNETCORE_ENVIRONMENT = "Development" -->
    <link rel="stylesheet" href="/css/main.css">
    <script type="module" src="https://localhost:5173/@vite/client"></script>
    <script type="module" src="https://localhost:5173/main.tsx"></script>
    <!-- From index.cshtml ^^^^ -->
    <script defer="" type="module" src="https://localhost:5173/ThemedFavicon.ts"></script>
</head>

My understanding from the documentation is that in "Production" the vite-src tag helper should render the exact same things, but with paths corrected to read from wwwroot. However, when I try that, I get this instead and my site is obviously deeply broken for lack of access to the main script.

<head>
    <link rel="icon" type="image/x-icon" href="/images/favicon-light.ico">
     <!-- From index.cshtml vvvv -->
     <!-- ASPNETCORE_ENVIRONMENT = "Production" -->
     <link rel="stylesheet" href="/css/main.css">
     <!-- From index.cshtml ^^^^ -->
</head>

I tried working around this by using this pattern:

@inject IViteManifest Manifest

<environment include="Development">
    <!-- Vite development server script -->
    <script type="module" src="http://localhost:5173/@@vite/client"></script>
    <script type="module" src="~/main.tsx"></script>
</environment>
<environment include="Production">
    <script type="module" src="~/@Manifest["main.tsx"]!.File" asp-append-version="true"></script>
</environment>

but I ran into the same problem: works with "Development", but not with "Production". When I inspected the value of @Manifest in the debugger I found that it was an enumeration that had no results. This is surprising because when I look in wwwroot I can see assests.manifest.json and it looks well-formed to my Vite-newbie eyes. In fact, I'm pretty confident that it's well-formed because my site works when I read the manifest myself and pass it to the Razor code through my page model like so:

    public IndexModel(IViteManifest mainfest, IWebHostEnvironment environment)
    {
        this.Manifest = mainfest;
        this.Environment = environment;
        string manifestPath = Path.Combine(this.Environment.WebRootPath, "assets.manifest.json");

        if (System.IO.File.Exists(manifestPath))
        {
            string manifestContent = System.IO.File.ReadAllText(manifestPath);
            JsonSerializerOptions deserializationOptions = new() { PropertyNameCaseInsensitive = true };
            this.ManifestEntries = JsonSerializer.Deserialize<Dictionary<string, ViteChunk>>(manifestContent, deserializationOptions) ?? [];
        }
    }

    /* Note that ViteChunk is my own trivial POCO that I created to provide an implementation for IViteChunck
        to deserialize the manifest */
=-=-=-=
        <script type="module" src="~/@Model.ManifestEntries["main.tsx"]!.File" asp-append-version="true"></script>

So, there's clearly a bug somewhere. I'm not sure if it's in my code (wouldn't surprise me) or in Vite.AspNetCore (which I assume is unlikely given the high-level nature of the issue). Here's my vite.config.ts file (for brevity I've chopped out the stuff related to creating the SSL certifcate for localhost since it seems unrelated and is working):

import { UserConfig, defineConfig } from 'vite';

import fs from 'fs';
import path from 'path';
import { spawn } from 'child_process';

// @ts-ignore
import appsettings from "./appsettings.json";
// @ts-ignore
import appsettingsDev from "./appsettings.Development.json";

// Pattern for CSS files
const cssPattern = /\.css$/;
// Pattern for image files
const imagePattern = /\.(png|jpe?g|gif|svg|webp|avif)$/;

// Export Vite configuration
export default defineConfig(async () => {

  // Define Vite configuration
  const config: UserConfig = {
    appType: 'custom',
    root: 'Assets',
    base: "/",
    publicDir: 'public',
    build: {
      outDir: '../wwwroot',
      emptyOutDir: true,
      manifest: appsettings.Vite.Manifest,
      assetsDir: "Assets",
      rollupOptions: {
        input: 'Assets/main.tsx',
        output: {
          // Save entry files to the appropriate folder
          entryFileNames: 'js/[name].[hash].js',
          // Save chunk files to the js folder
          chunkFileNames: 'js/[name]-chunk.js',
          // Save asset files to the appropriate folder
          assetFileNames: (info) => {
            if (info.name) {
              // If the file is a CSS file, save it to the css folder
              if (cssPattern.test(info.name)) {
                return 'css/[name][extname]';
              }
              // If the file is an image file, save it to the images folder
              if (imagePattern.test(info.name)) {
                return 'images/[name][extname]';
              }

              // If the file is any other type of file, save it to the assets folder
              return 'assets/[name][extname]';
            } else {
              // If the file name is not specified, save it to the output directory
              return '[name][extname]';
            }
          },
        } 
      },
    },
    server: {
      port: appsettingsDev.Vite.Server.Port,
      strictPort: true,
      https: {
        cert: certFilePath,
        key: keyFilePath
      },
      hmr: {
        host: "localhost",
        clientPort: appsettingsDev.Vite.Server.Port
      }
    },
    optimizeDeps: {
      include: []
    }
  }

  return config;
});
Eptagone commented 6 months ago

Hi, which value are you using in the Manifest option?

BernardHymmen commented 6 months ago

I'm using `"Vite:Manifest": "assets.manifest.json" (and just doublechecked it for typos :) ). Here's my appsettings.json file (minus the app-specific stuff):

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "System.Net.Http.HttpClient.Vite.AspNetCore.DevHttpClient": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Vite": {
    "Manifest": "assets.manifest.json"
  }
}
Eptagone commented 6 months ago

Can I see your Program.cs file? Only the Vite stuff.

BernardHymmen commented 6 months ago

Sure! You'll notice there's some code at the top to set ASPNETCORE_ENVIRONMENT based on the build configuration, but the problem I'm reporting here was happening before that code went in, so I don't think it's contributing (plus it seems to be working just fine otherwise and has proven very handy for me). Other than that, there isn't anything special or proprietary going on, so this is my entire Program.cs file.

using System.Reflection;
using Vite.AspNetCore;

// Set the value for ASPNETCORE_ENVIRONMENT based on build configuration if it's not already set externally
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")))
{
    AssemblyConfigurationAttribute? assemblyConfigurationAttribute = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyConfigurationAttribute>();
    string? buildConfigurationName = assemblyConfigurationAttribute?.Configuration;
    Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT",
        string.Equals(buildConfigurationName, "Debug", StringComparison.OrdinalIgnoreCase)
        ? "Development"
        : "Production");
}

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddViteServices(options =>
{
    options.Server.AutoRun = true;
    options.Server.Https = true;
});

WebApplication app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();

app.MapRazorPages();

if (app.Environment.IsDevelopment())
{
    app.UseWebSockets();
    app.UseViteDevelopmentServer(true);
}

app.UseStatusCodePagesWithReExecute("/index");

app.Run();
Eptagone commented 6 months ago

Ahh you are passing options to the AddViteServices method. If you do that, then the library will use those values instead. Use the method without parameters or add the manifest name to the method.

builder.Services.AddViteServices(options =>
{
    options.Manifest = "assets.manifest.json";
    options.Server.AutoRun = true;
    options.Server.Https = true;
});
BernardHymmen commented 6 months ago

Ah HA! Yes, adding options.Manifest = ... did the trick. Thank you so much for taking the time to help me!

BernardHymmen commented 6 months ago

(Whoops, I meant to close this issue with my last reply.)