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
1 change: 1 addition & 0 deletions packages/angular/build/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ ts_project(
":node_modules/@babel/core",
":node_modules/oxc-parser",
"//:node_modules/@angular/compiler-cli",
"//:node_modules/@angular/router",
"//:node_modules/@types/jasmine",
"//:node_modules/esbuild",
"//:node_modules/prettier",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {

export function createAngularSsrInternalMiddleware(
server: ViteDevServer,
resetComponentUpdates: () => void,
indexHtmlTransformer?: (content: string) => Promise<string>,
): Connect.NextHandleFunction {
let cachedAngularServerApp: ReturnType<typeof getOrCreateAngularServerApp> | undefined;
Expand Down Expand Up @@ -53,6 +54,7 @@ export function createAngularSsrInternalMiddleware(
// Only Add the transform hook only if it's a different instance.
if (cachedAngularServerApp !== angularServerApp) {
angularServerApp.hooks.on('html:transform:pre', async ({ html, url }) => {
resetComponentUpdates();
const processedHtml = await server.transformIndexHtml(url.pathname, html);

return indexHtmlTransformer?.(processedHtml) ?? processedHtml;
Expand All @@ -76,13 +78,13 @@ export function createAngularSsrInternalMiddleware(

export async function createAngularSsrExternalMiddleware(
server: ViteDevServer,
resetComponentUpdates: () => void,
indexHtmlTransformer?: (content: string) => Promise<string>,
): Promise<Connect.NextHandleFunction> {
let fallbackWarningShown = false;
let cachedAngularAppEngine: typeof SSRAngularAppEngine | undefined;
let angularSsrInternalMiddleware:
| ReturnType<typeof createAngularSsrInternalMiddleware>
| undefined;
ReturnType<typeof createAngularSsrInternalMiddleware> | undefined;

// Load the compiler because `@angular/ssr/node` depends on `@angular/` packages,
// which must be processed by the runtime linker, even if they are not used.
Expand Down Expand Up @@ -120,6 +122,7 @@ export async function createAngularSsrExternalMiddleware(

angularSsrInternalMiddleware ??= createAngularSsrInternalMiddleware(
server,
resetComponentUpdates,
indexHtmlTransformer,
);

Expand All @@ -132,6 +135,7 @@ export async function createAngularSsrExternalMiddleware(
AngularAppEngine.ɵdisableAllowedHostsCheck = disableAllowedHostsCheck;
AngularAppEngine.ɵallowStaticRouteRender = true;
AngularAppEngine.ɵhooks.on('html:transform:pre', async ({ html, url }) => {
resetComponentUpdates();
const processedHtml = await server.transformIndexHtml(url.pathname, html);

return indexHtmlTransformer?.(processedHtml) ?? processedHtml;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* @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 {
createAngularSsrExternalMiddleware,
createAngularSsrInternalMiddleware,
} from './ssr-middleware';

type ViteDevServer = Parameters<typeof createAngularSsrInternalMiddleware>[0];

describe('SSR dev-server middleware', () => {
function createMockReqRes(): { req: IncomingMessage; res: ServerResponse } {
const req = {
url: '/',
method: 'GET',
headers: { host: 'localhost' },
} as unknown as IncomingMessage;

const res = {
statusCode: 200,
setHeader: jasmine.createSpy('setHeader'),
writeHead: jasmine.createSpy('writeHead'),
end: jasmine.createSpy('end'),
} as unknown as ServerResponse;

return { req, res };
}

it('resets component updates when html:transform:pre executes in internal SSR middleware', async () => {
let hookCallback: ((args: { html: string; url: URL }) => Promise<string>) | undefined;
const resetComponentUpdates = jasmine.createSpy('resetComponentUpdates');
let resolveHookRegistered!: () => void;
const hookRegistered = new Promise<void>((resolve) => {
resolveHookRegistered = resolve;
});

const fakeAngularServerApp = {
hooks: {
on: (_name: string, cb: typeof hookCallback) => {
hookCallback = cb;
resolveHookRegistered();
},
},
handle: jasmine.createSpy('handle').and.resolveTo(new Response('rendered')),
};

const server = {
config: { server: { allowedHosts: true }, base: '/' },
ssrLoadModule: jasmine.createSpy('ssrLoadModule').and.resolveTo({
ɵgetOrCreateAngularServerApp: () => fakeAngularServerApp,
}),
transformIndexHtml: jasmine
.createSpy('transformIndexHtml')
.and.callFake(async (_path: string, html: string) => html),
} as unknown as ViteDevServer;

const middleware = createAngularSsrInternalMiddleware(
server,
resetComponentUpdates,
async (html) => html + '<!-- custom -->',
);

const { req, res } = createMockReqRes();
middleware(req, res, () => {});

await hookRegistered;

expect(resetComponentUpdates).not.toHaveBeenCalled();
Comment thread
alan-agius4 marked this conversation as resolved.
expect(hookCallback).toBeDefined();
const result = await hookCallback?.({
html: '<div>test</div>',
url: new URL('http://localhost/'),
});
expect(resetComponentUpdates).toHaveBeenCalledTimes(1);
expect(result).toBe('<div>test</div><!-- custom -->');
});

it('resets component updates when html:transform:pre executes in external SSR middleware', async () => {
let hookCallback: ((args: { html: string; url: URL }) => Promise<string>) | undefined;
const resetComponentUpdates = jasmine.createSpy('resetComponentUpdates');
let resolveHookRegistered!: () => void;
const hookRegistered = new Promise<void>((resolve) => {
resolveHookRegistered = resolve;
});

class FakeAngularAppEngine {
static ɵhooks = {
on: (_name: string, cb: typeof hookCallback) => {
hookCallback = cb;
resolveHookRegistered();
},
};
static ɵdisableAllowedHostsCheck: boolean;
static ɵallowStaticRouteRender: boolean;
}

const server = {
config: { server: { allowedHosts: true }, base: '/' },
ssrLoadModule: jasmine.createSpy('ssrLoadModule').and.resolveTo({
reqHandler: Object.assign(() => new Response('ok'), { __ng_request_handler__: true }),
AngularAppEngine: FakeAngularAppEngine,
}),
transformIndexHtml: jasmine
.createSpy('transformIndexHtml')
.and.callFake(async (_path: string, html: string) => html + '<!-- transformed -->'),
} as unknown as ViteDevServer;

const middleware = await createAngularSsrExternalMiddleware(server, resetComponentUpdates);

const { req, res } = createMockReqRes();
middleware(req, res, () => {});

await hookRegistered;

expect(resetComponentUpdates).not.toHaveBeenCalled();
Comment thread
alan-agius4 marked this conversation as resolved.
expect(hookCallback).toBeDefined();
const result = await hookCallback?.({
html: '<div>test</div>',
url: new URL('http://localhost/'),
});
expect(resetComponentUpdates).toHaveBeenCalledTimes(1);
expect(result).toBe('<div>test</div><!-- transformed -->');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,21 @@ export function createAngularSetupMiddlewaresPlugin(

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

return;
}

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

middlewares.use(angularHtmlFallbackMiddleware);
Expand Down
Loading