Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import type { IncomingMessage, ServerResponse } from 'node:http';
import type { Connect } from 'vite';
import { addLeadingSlash } from '../../../utils/url';

/**
* Patches the Vite base middleware to correctly handle the Angular application's base href.
* This is necessary because Vite's default base middleware might not align with Angular's
* expected path handling when using SSR, especially when a base href is configured.
*
* @param middlewares The Connect server instance containing the middleware stack.
* @param base The base URL path to be handled by the middleware.
*/
export function patchBaseMiddleware(middlewares: Connect.Server, base: string): void {
const entry = middlewares.stack.find(
({ handle }) => typeof handle === 'function' && handle.name.startsWith('viteBaseMiddleware'),
);

if (typeof entry?.handle !== 'function') {
return;
}

entry.handle = function angularBaseMiddleware(
req: IncomingMessage,
res: ServerResponse,
next: (err?: unknown) => void,
) {
const url = req.url || '/';
if (url.startsWith(base)) {
// Rewrite the URL to remove the base prefix before passing it to the next middleware.
// If the URL is exactly the base, it becomes '/'.
// Otherwise, we slice off the base and ensure there's a leading slash.
// See: https://github.com/vitejs/vite/blob/e81c183f8c8ccaf7774ef0d0ee125bf63dbf30b4/packages/vite/src/node/server/middlewares/base.ts#L12
req.url = url === base ? '/' : addLeadingSlash(url.slice(base.length - 1));

return next();
}

const { pathname, hash, search } = new URL(url, 'http://localhost');
if (pathname === '/' || pathname === '/index.html') {
res.writeHead(302, { Location: `${base}${search}${hash}` });
res.end();

return;
}
Comment on lines +46 to +52
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the existing fallback middleware handle this case?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


next();
};
}
1 change: 1 addition & 0 deletions packages/angular/build/src/tools/vite/middlewares/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export { createAngularHeadersMiddleware } from './headers-middleware';
export { createAngularComponentMiddleware } from './component-middleware';
export { createChromeDevtoolsMiddleware } from './chrome-devtools-middleware';
export { patchHostValidationMiddleware } from './host-check-middleware';
export { patchBaseMiddleware } from './base-middleware';
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
createAngularSsrExternalMiddleware,
createAngularSsrInternalMiddleware,
createChromeDevtoolsMiddleware,
patchBaseMiddleware,
patchHostValidationMiddleware,
} from '../middlewares';
import { AngularMemoryOutputFiles, AngularOutputAssets } from '../utils';
Expand Down Expand Up @@ -87,10 +88,12 @@ export function createAngularSetupMiddlewaresPlugin(
resetComponentUpdates,
} = options;

const middlewares = server.middlewares;

// Headers, assets and resources get handled first
server.middlewares.use(createAngularHeadersMiddleware(server));
server.middlewares.use(createAngularComponentMiddleware(server, templateUpdates));
server.middlewares.use(
middlewares.use(createAngularHeadersMiddleware(server));
middlewares.use(createAngularComponentMiddleware(server, templateUpdates));
middlewares.use(
createAngularAssetsMiddleware(
server,
assets,
Expand All @@ -100,11 +103,9 @@ export function createAngularSetupMiddlewaresPlugin(
),
);

server.middlewares.use(
createChromeDevtoolsMiddleware(server.config.cacheDir, options.projectRoot),
);
middlewares.use(createChromeDevtoolsMiddleware(server.config.cacheDir, options.projectRoot));

extensionMiddleware?.forEach((middleware) => server.middlewares.use(middleware));
extensionMiddleware?.forEach((middleware) => middlewares.use(middleware));

// Returning a function, installs middleware after the main transform middleware but
// before the built-in HTML middleware
Expand All @@ -113,19 +114,18 @@ export function createAngularSetupMiddlewaresPlugin(
patchHostValidationMiddleware(server.middlewares);

if (ssrMode === ServerSsrMode.ExternalSsrMiddleware) {
server.middlewares.use(
await createAngularSsrExternalMiddleware(server, indexHtmlTransformer),
);
patchBaseMiddleware(server.middlewares, server.config.base);
middlewares.use(await createAngularSsrExternalMiddleware(server, indexHtmlTransformer));

return;
}

if (ssrMode === ServerSsrMode.InternalSsrMiddleware) {
server.middlewares.use(createAngularSsrInternalMiddleware(server, indexHtmlTransformer));
middlewares.use(createAngularSsrInternalMiddleware(server, indexHtmlTransformer));
}

server.middlewares.use(angularHtmlFallbackMiddleware);
server.middlewares.use(
middlewares.use(angularHtmlFallbackMiddleware);
middlewares.use(
createAngularIndexHtmlMiddleware(
server,
outputFiles,
Expand Down
48 changes: 48 additions & 0 deletions tests/legacy-cli/e2e/tests/vite/ssr-base-href.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import assert from 'node:assert';
import { ng } from '../../utils/process';
import { replaceInFile } from '../../utils/fs';
import { installWorkspacePackages, uninstallPackage } from '../../utils/packages';
import { ngServe, updateJsonFile, useSha } from '../../utils/project';
import { getGlobalVariable } from '../../utils/env';

export default async function () {
assert(
getGlobalVariable('argv')['esbuild'],
'This test should not be called in the Webpack suite.',
);

await uninstallPackage('@angular/ssr');
await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
await useSha();
await installWorkspacePackages();

await updateJsonFile('angular.json', (json) => {
json.projects['test-project'].architect.build.options['baseHref'] = '/base';
});

await replaceInFile(
'src/server.ts',
/express\(\);/,
`express();

app.use('/ping', (req, res) => {
return res.json({ pong: true });
});`,
);

const port = await ngServe();

// Angular application and bundled should be affected by baseHref
await matchResponse(`http://localhost:${port}/base`, /ng-server-context/);
await matchResponse(`http://localhost:${port}/base/main.js`, /App/);

// Server endpoint should not be affected by baseHref
await matchResponse(`http://localhost:${port}/ping`, /pong/);
}

async function matchResponse(url: string, match: RegExp): Promise<void> {
const response = await fetch(url);
const text = await response.text();

assert.match(text, match);
}