diff --git a/packages/angular_devkit/build_angular/src/dev-server/index.ts b/packages/angular_devkit/build_angular/src/dev-server/index.ts index db0e5a4801c8..48a5dfd1d1b8 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/index.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/index.ts @@ -259,10 +259,16 @@ export function buildServerConfig( host: serverOptions.host, port: serverOptions.port, headers: { 'Access-Control-Allow-Origin': '*' }, - historyApiFallback: { + historyApiFallback: !!browserOptions.index && { index: `${servePath}/${path.basename(browserOptions.index)}`, disableDotRule: true, htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'], + rewrites: [ + { + from: new RegExp(`^(?!${servePath})/.*`), + to: context => url.format(context.parsedUrl), + }, + ], } as WebpackDevServer.HistoryApiFallbackConfig, stats: false, compress: styles || scripts, diff --git a/packages/angular_devkit/build_angular/test/dev-server/serve-path_spec_large.ts b/packages/angular_devkit/build_angular/test/dev-server/serve-path_spec_large.ts index 3f5781c09698..21e7e11ec112 100644 --- a/packages/angular_devkit/build_angular/test/dev-server/serve-path_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/dev-server/serve-path_spec_large.ts @@ -27,7 +27,7 @@ describe('Dev Server Builder serve path', () => { await Promise.all(runs.map(r => r.stop())); }); - it('works', async () => { + it('uses the servePath option when specified', async () => { const run = await architect.scheduleTarget(target, { servePath: 'test/' }); runs.push(run); const output = await run.result as DevServerBuilderOutput; @@ -37,4 +37,39 @@ describe('Dev Server Builder serve path', () => { const response = await fetch(`${output.baseUrl}/polyfills.js`); expect(await response.text()).toContain('window["webpackJsonp"]'); }, 30000); + + it('does not fallback when request is outside serve path', async () => { + const run = await architect.scheduleTarget(target, { servePath: 'test/' }); + + await expectAsync(run.result).toBeResolvedTo( + jasmine.objectContaining({ success: true, baseUrl: 'http://localhost:4200/test' }), + ); + + // fallback processing requires an accept header + await expectAsync( + fetch('http://localhost:4200', { headers: { 'accept': 'text/html' } }), + ).toBeResolvedTo(jasmine.objectContaining({ status: 404 })); + + await expectAsync( + fetch('http://localhost:4200/api/', { headers: { 'accept': 'text/html' } }), + ).toBeResolvedTo(jasmine.objectContaining({ status: 404 })); + + await run.stop(); + }, 30000); + + it('does fallback when request is inside serve path', async () => { + const run = await architect.scheduleTarget(target, { servePath: 'test/' }); + + await expectAsync(run.result).toBeResolvedTo( + jasmine.objectContaining({ success: true, baseUrl: 'http://localhost:4200/test' }), + ); + + // fallback processing requires an accept header + await expectAsync( + fetch('http://localhost:4200/test/nothere', { headers: { 'accept': 'text/html' } }), + ).toBeResolvedTo(jasmine.objectContaining({ status: 200 })); + + await run.stop(); + }, 30000); + });