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',
+ '
Login page
',
+ );
+
+ 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('Login page
');
+ });
+
+ (isVite ? it : xit)(
+ `should return the asset that matches '.html' when path has no trailing '/'`,
+ async () => {
+ await harness.writeFile(
+ 'src/login/new.html',
+ 'Login page
',
+ );
+
+ 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('Login page
');
+ },
+ );
});
});
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.