diff --git a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/options/serve-path_spec.ts b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/options/serve-path_spec.ts index c80aca3670c1..734637c5aead 100644 --- a/packages/angular_devkit/build_angular/src/builders/dev-server/tests/options/serve-path_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/dev-server/tests/options/serve-path_spec.ts @@ -18,7 +18,9 @@ describeServeBuilder( (harness, setupTarget, isViteRun) => { describe('option: "servePath"', () => { beforeEach(async () => { - setupTarget(harness); + setupTarget(harness, { + assets: ['src/assets'], + }); // Application code is not needed for these tests await harness.writeFile('src/main.ts', 'console.log("foo");'); @@ -96,6 +98,23 @@ describeServeBuilder( expect(result?.success).toBeTrue(); expect(await response?.text()).toContain(''); }); + + it('serves assets at specified path when option is used', async () => { + await harness.writeFile('src/assets/test.txt', 'hello world!'); + + harness.useTarget('serve', { + ...BASE_OPTIONS, + servePath: 'test', + }); + + const { result, response } = await executeOnceAndFetch(harness, '/test/assets/test.txt', { + // fallback processing requires an accept header + request: { headers: { accept: 'text/html' } }, + }); + + expect(result?.success).toBeTrue(); + expect(await response?.text()).toContain('hello world'); + }); }); }, ); 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 13f92d427110..055ccdfbcecb 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 @@ -448,6 +448,7 @@ export async function setupServer( css: { devSourcemap: true, }, + // Vite will normalize the `base` option by adding a leading and trailing forward slash. base: serverOptions.servePath, resolve: { mainFields: ['es2020', 'browser', 'module', 'main'], @@ -583,7 +584,7 @@ export async function setupServer( // Parse the incoming request. // The base of the URL is unused but required to parse the URL. - const pathname = pathnameWithoutServePath(req.url, serverOptions); + const pathname = pathnameWithoutBasePath(req.url, server.config.base); const extension = extname(pathname); // Rewrite all build assets to a vite raw fs URL @@ -591,7 +592,7 @@ export async function setupServer( if (assetSourcePath !== undefined) { // The encoding needs to match what happens in the vite static middleware. // ref: https://github.com/vitejs/vite/blob/d4f13bd81468961c8c926438e815ab6b1c82735e/packages/vite/src/node/server/middlewares/static.ts#L163 - req.url = `/@fs/${encodeURI(assetSourcePath)}`; + req.url = `${server.config.base}@fs/${encodeURI(assetSourcePath)}`; next(); return; @@ -689,7 +690,7 @@ export async function setupServer( // Parse the incoming request. // The base of the URL is unused but required to parse the URL. - const pathname = pathnameWithoutServePath(req.url, serverOptions); + const pathname = pathnameWithoutBasePath(req.url, server.config.base); if (pathname === '/' || pathname === `/index.html`) { const rawHtml = outputFiles.get('/index.html')?.contents; @@ -801,17 +802,12 @@ async function loadViteClientCode(file: string) { return contents; } -function pathnameWithoutServePath(url: string, serverOptions: NormalizedDevServerOptions): string { +function pathnameWithoutBasePath(url: string, basePath: string): string { const parsedUrl = new URL(url, 'http://localhost'); - let pathname = decodeURIComponent(parsedUrl.pathname); - if (serverOptions.servePath && pathname.startsWith(serverOptions.servePath)) { - pathname = pathname.slice(serverOptions.servePath.length); - if (pathname[0] !== '/') { - pathname = '/' + pathname; - } - } + const pathname = decodeURIComponent(parsedUrl.pathname); - return pathname; + // slice(basePath.length - 1) to retain the trailing slash + return pathname.startsWith(basePath) ? pathname.slice(basePath.length - 1) : pathname; } type ViteEsBuildPlugin = NonNullable<