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.