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 cdb2fd4509e1..01b06d52d2af 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 @@ -41,6 +41,21 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT setupTarget(harness, { assets: ['src/extra.ts'], + }); + + harness.useTarget('serve', { + ...BASE_OPTIONS, + }); + + const { result, response } = await executeOnceAndFetch(harness, 'extra.ts'); + + expect(result?.success).toBeTrue(); + expect(await response?.text()).toContain(javascriptFileContent); + }); + + it('should return 404 for non existing assets', async () => { + setupTarget(harness, { + assets: ['src/extra.js'], optimization: { scripts: true, }, @@ -50,10 +65,10 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT ...BASE_OPTIONS, }); - const { result, response } = await executeOnceAndFetch(harness, 'extra.ts'); + const { result, response } = await executeOnceAndFetch(harness, 'extra.js'); expect(result?.success).toBeTrue(); - expect(await response?.text()).toContain(javascriptFileContent); + expect(await response?.status).toBe(404); }); }); }); 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 a32b15efb6d1..a042a6e4e8f9 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 @@ -462,7 +462,7 @@ export async function setupServer( publicDir: false, esbuild: false, mode: 'development', - appType: 'spa', + appType: 'mpa', 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 b9039c2027dd..bb40260e827b 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 @@ -90,6 +90,7 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions): map: mapContents && Buffer.from(mapContents).toString('utf-8'), }; }, + // eslint-disable-next-line max-lines-per-function configureServer(server) { const originalssrTransform = server.ssrTransform; server.ssrTransform = async (code, map, url, originalCode) => { @@ -169,6 +170,8 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions): // Returning a function, installs middleware after the main transform middleware but // before the built-in HTML middleware return () => { + server.middlewares.use(angularHtmlFallbackMiddleware); + function angularSSRMiddleware( req: Connect.IncomingMessage, res: ServerResponse, @@ -180,8 +183,8 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions): // Skip if path is not defined. !url || // Skip if path is like a file. - // NOTE: We use a regexp to mitigate against matching requests like: /browse/pl.0ef59752c0cd457dbf1391f08cbd936f - /^\.[a-z]{2,4}$/i.test(extname(url.split('?')[0])) + // NOTE: We use a mime type lookup to mitigate against matching requests like: /browse/pl.0ef59752c0cd457dbf1391f08cbd936f + lookupMimeTypeFromRequest(url) ) { next(); @@ -306,3 +309,34 @@ function pathnameWithoutBasePath(url: string, basePath: string): string { ? pathname.slice(basePath.length - 1) : pathname; } + +function angularHtmlFallbackMiddleware( + req: Connect.IncomingMessage, + res: ServerResponse, + next: Connect.NextFunction, +): void { + // Similar to how it is handled in vite + // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/middlewares/htmlFallback.ts#L15C19-L15C45 + if ( + (req.method === 'GET' || req.method === 'HEAD') && + (!req.url || !lookupMimeTypeFromRequest(req.url)) && + (!req.headers.accept || + req.headers.accept.includes('text/html') || + req.headers.accept.includes('text/*') || + req.headers.accept.includes('*/*')) + ) { + req.url = '/index.html'; + } + + next(); +} + +function lookupMimeTypeFromRequest(url: string): string | undefined { + const extension = extname(url.split('?')[0]); + + if (extension === '.ico') { + return 'image/x-icon'; + } + + return extension && lookupMimeType(extension); +}