From 8d5af1d5c78ce9aa996f6ba138b99d0bb5005d46 Mon Sep 17 00:00:00 2001
From: Alan Agius <alanagius@google.com>
Date: Wed, 7 Feb 2024 09:12:07 +0000
Subject: [PATCH] fix(@angular-devkit/build-angular): ensure correct `.html`
 served with Vite dev-server

Prior to this commit, the Vite html fallback middleware failed to handle the in-memory assets generated by Angular CLI, resulting in incorrect fallback behavior. For instance, when an `index.html` existed as an asset under a specific path, the generated `index.html` would be served instead.

This fix addresses the issue, ensuring that the appropriate `.html` is served when using the Vite dev-server.

Closes #27044
---
 .../tests/behavior/build-assets_spec.ts       | 71 ++++++++++++++++++-
 .../src/builders/dev-server/vite-server.ts    |  3 +-
 .../src/tools/vite/angular-memory-plugin.ts   | 17 +++++
 3 files changed, 89 insertions(+), 2 deletions(-)

diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build-assets_spec.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build-assets_spec.ts
index 01b06d52d2af..23dc7722647f 100644
--- a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build-assets_spec.ts
+++ b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/behavior/build-assets_spec.ts
@@ -11,7 +11,7 @@ import { executeOnceAndFetch } from '../execute-fetch';
 import { describeServeBuilder } from '../jasmine-helpers';
 import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
 
-describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => {
+describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget, isVite) => {
   const javascriptFileContent =
     "import {foo} from 'unresolved'; /* a comment */const foo = `bar`;\n\n\n";
 
@@ -70,5 +70,74 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT
       expect(result?.success).toBeTrue();
       expect(await response?.status).toBe(404);
     });
+
+    it('should return 404 for non existing assets', async () => {
+      setupTarget(harness, {
+        assets: ['src/extra.js'],
+        optimization: {
+          scripts: true,
+        },
+      });
+
+      harness.useTarget('serve', {
+        ...BASE_OPTIONS,
+      });
+
+      const { result, response } = await executeOnceAndFetch(harness, 'extra.js');
+
+      expect(result?.success).toBeTrue();
+      expect(await response?.status).toBe(404);
+    });
+
+    it(`should return the asset that matches 'index.html' when path has a trailing '/'`, async () => {
+      await harness.writeFile(
+        'src/login/index.html',
+        '<html><body><h1>Login page</h1></body><html>',
+      );
+
+      setupTarget(harness, {
+        assets: ['src/login'],
+        optimization: {
+          scripts: true,
+        },
+      });
+
+      harness.useTarget('serve', {
+        ...BASE_OPTIONS,
+      });
+
+      const { result, response } = await executeOnceAndFetch(harness, 'login/');
+
+      expect(result?.success).toBeTrue();
+      expect(await response?.status).toBe(200);
+      expect(await response?.text()).toContain('<h1>Login page</h1>');
+    });
+
+    (isVite ? it : xit)(
+      `should return the asset that matches '.html' when path has no trailing '/'`,
+      async () => {
+        await harness.writeFile(
+          'src/login/new.html',
+          '<html><body><h1>Login page</h1></body><html>',
+        );
+
+        setupTarget(harness, {
+          assets: ['src/login'],
+          optimization: {
+            scripts: true,
+          },
+        });
+
+        harness.useTarget('serve', {
+          ...BASE_OPTIONS,
+        });
+
+        const { result, response } = await executeOnceAndFetch(harness, 'login/new');
+
+        expect(result?.success).toBeTrue();
+        expect(await response?.status).toBe(200);
+        expect(await response?.text()).toContain('<h1>Login page</h1>');
+      },
+    );
   });
 });
diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts
index 9951754fb6f2..4cdad6e6da80 100644
--- a/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts
+++ b/packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts
@@ -461,7 +461,8 @@ export async function setupServer(
     publicDir: false,
     esbuild: false,
     mode: 'development',
-    appType: 'mpa',
+    // We use custom as we do not rely on Vite's htmlFallbackMiddleware and indexHtmlMiddleware.
+    appType: 'custom',
     css: {
       devSourcemap: true,
     },
diff --git a/packages/angular_devkit/build_angular/src/tools/vite/angular-memory-plugin.ts b/packages/angular_devkit/build_angular/src/tools/vite/angular-memory-plugin.ts
index 8f9a12d1676b..294c8cd7295b 100644
--- a/packages/angular_devkit/build_angular/src/tools/vite/angular-memory-plugin.ts
+++ b/packages/angular_devkit/build_angular/src/tools/vite/angular-memory-plugin.ts
@@ -140,6 +140,23 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
           return;
         }
 
+        // HTML fallbacking
+        // This matches what happens in the vite html fallback middleware.
+        // ref: https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/middlewares/htmlFallback.ts#L9
+        const htmlAssetSourcePath =
+          pathname[pathname.length - 1] === '/'
+            ? // Trailing slash check for `index.html`.
+              assets.get(pathname + 'index.html')
+            : // Non-trailing slash check for fallback `.html`
+              assets.get(pathname + '.html');
+
+        if (htmlAssetSourcePath) {
+          req.url = `${server.config.base}@fs/${encodeURI(htmlAssetSourcePath)}`;
+          next();
+
+          return;
+        }
+
         // Resource files are handled directly.
         // Global stylesheets (CSS files) are currently considered resources to workaround
         // dev server sourcemap issues with stylesheets.