Skip to content

Commit a1222f3

Browse files
committed
refactor(@angular/build): move Vite middlewares into separate files
As the number of middlewares has increased over time, this commit enhances code health by relocating them into individual files. (cherry picked from commit b5af8b5)
1 parent 27bd670 commit a1222f3

File tree

8 files changed

+354
-246
lines changed

8 files changed

+354
-246
lines changed

packages/angular/build/src/builders/dev-server/vite-server.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,6 @@ export async function setupServer(
572572
outputFiles,
573573
assets,
574574
ssr,
575-
extraHeaders: serverOptions.headers,
576575
external: externalMetadata.explicit,
577576
indexHtmlTransformer,
578577
extensionMiddleware,

packages/angular/build/src/tools/vite/angular-memory-plugin.ts

Lines changed: 16 additions & 245 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,30 @@
77
*/
88

99
import remapping, { SourceMapInput } from '@ampproject/remapping';
10-
import { lookup as lookupMimeType } from 'mrmime';
1110
import assert from 'node:assert';
1211
import { readFile } from 'node:fs/promises';
13-
import { ServerResponse } from 'node:http';
14-
import { dirname, extname, join, relative } from 'node:path';
12+
import { dirname, join, relative } from 'node:path';
1513
import type { Connect, Plugin } from 'vite';
16-
import { renderPage } from '../../utils/server-rendering/render-page';
14+
import {
15+
angularHtmlFallbackMiddleware,
16+
createAngularAssetsMiddleware,
17+
createAngularIndexHtmlMiddleware,
18+
createAngularSSRMiddleware,
19+
} from './middlewares';
20+
import { AngularMemoryOutputFiles } from './utils';
1721

1822
export interface AngularMemoryPluginOptions {
1923
workspaceRoot: string;
2024
virtualProjectRoot: string;
21-
outputFiles: Map<string, { contents: Uint8Array; servable: boolean }>;
25+
outputFiles: AngularMemoryOutputFiles;
2226
assets: Map<string, string>;
2327
ssr: boolean;
2428
external?: string[];
2529
extensionMiddleware?: Connect.NextHandleFunction[];
26-
extraHeaders?: Record<string, string>;
2730
indexHtmlTransformer?: (content: string) => Promise<string>;
2831
normalizePath: (path: string) => string;
2932
}
3033

31-
// eslint-disable-next-line max-lines-per-function
3234
export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions): Plugin {
3335
const {
3436
workspaceRoot,
@@ -38,7 +40,6 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
3840
external,
3941
ssr,
4042
extensionMiddleware,
41-
extraHeaders,
4243
indexHtmlTransformer,
4344
normalizePath,
4445
} = options;
@@ -112,84 +113,7 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
112113
};
113114

114115
// Assets and resources get handled first
115-
server.middlewares.use(function angularAssetsMiddleware(req, res, next) {
116-
if (req.url === undefined || res.writableEnded) {
117-
return;
118-
}
119-
120-
// Parse the incoming request.
121-
// The base of the URL is unused but required to parse the URL.
122-
const pathname = pathnameWithoutBasePath(req.url, server.config.base);
123-
const extension = extname(pathname);
124-
const pathnameHasTrailingSlash = pathname[pathname.length - 1] === '/';
125-
126-
// Rewrite all build assets to a vite raw fs URL
127-
const assetSourcePath = assets.get(pathname);
128-
if (assetSourcePath !== undefined) {
129-
// Workaround to disable Vite transformer middleware.
130-
// See: https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/middlewares/transform.ts#L201 and
131-
// https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/transformRequest.ts#L204-L206
132-
req.headers.accept = 'text/html';
133-
134-
// The encoding needs to match what happens in the vite static middleware.
135-
// ref: https://github.com/vitejs/vite/blob/d4f13bd81468961c8c926438e815ab6b1c82735e/packages/vite/src/node/server/middlewares/static.ts#L163
136-
req.url = `${server.config.base}@fs/${encodeURI(assetSourcePath)}`;
137-
next();
138-
139-
return;
140-
}
141-
142-
// HTML fallbacking
143-
// This matches what happens in the vite html fallback middleware.
144-
// ref: https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/middlewares/htmlFallback.ts#L9
145-
const htmlAssetSourcePath = pathnameHasTrailingSlash
146-
? // Trailing slash check for `index.html`.
147-
assets.get(pathname + 'index.html')
148-
: // Non-trailing slash check for fallback `.html`
149-
assets.get(pathname + '.html');
150-
151-
if (htmlAssetSourcePath) {
152-
req.url = `${server.config.base}@fs/${encodeURI(htmlAssetSourcePath)}`;
153-
next();
154-
155-
return;
156-
}
157-
158-
// Resource files are handled directly.
159-
// Global stylesheets (CSS files) are currently considered resources to workaround
160-
// dev server sourcemap issues with stylesheets.
161-
if (extension !== '.js' && extension !== '.html') {
162-
const outputFile = outputFiles.get(pathname);
163-
if (outputFile?.servable) {
164-
const mimeType = lookupMimeType(extension);
165-
if (mimeType) {
166-
res.setHeader('Content-Type', mimeType);
167-
}
168-
res.setHeader('Cache-Control', 'no-cache');
169-
if (extraHeaders) {
170-
Object.entries(extraHeaders).forEach(([name, value]) => res.setHeader(name, value));
171-
}
172-
res.end(outputFile.contents);
173-
174-
return;
175-
}
176-
}
177-
178-
// If the path has no trailing slash and it matches a servable directory redirect to the same path with slash.
179-
// This matches the default express static behaviour.
180-
// See: https://github.com/expressjs/serve-static/blob/89fc94567fae632718a2157206c52654680e9d01/index.js#L182
181-
if (!pathnameHasTrailingSlash) {
182-
for (const assetPath of assets.keys()) {
183-
if (pathname === assetPath.substring(0, assetPath.lastIndexOf('/'))) {
184-
redirect(res, req.url + '/');
185-
186-
return;
187-
}
188-
}
189-
}
190-
191-
next();
192-
});
116+
server.middlewares.use(createAngularAssetsMiddleware(server, assets, outputFiles));
193117

194118
if (extensionMiddleware?.length) {
195119
extensionMiddleware.forEach((middleware) => server.middlewares.use(middleware));
@@ -200,111 +124,16 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
200124
return () => {
201125
server.middlewares.use(angularHtmlFallbackMiddleware);
202126

203-
function angularSSRMiddleware(
204-
req: Connect.IncomingMessage,
205-
res: ServerResponse,
206-
next: Connect.NextFunction,
207-
) {
208-
const url = req.originalUrl;
209-
if (
210-
!req.url ||
211-
// Skip if path is not defined.
212-
!url ||
213-
// Skip if path is like a file.
214-
// NOTE: We use a mime type lookup to mitigate against matching requests like: /browse/pl.0ef59752c0cd457dbf1391f08cbd936f
215-
lookupMimeTypeFromRequest(url)
216-
) {
217-
next();
218-
219-
return;
220-
}
221-
222-
const rawHtml = outputFiles.get('/index.server.html')?.contents;
223-
if (!rawHtml) {
224-
next();
225-
226-
return;
227-
}
228-
229-
transformIndexHtmlAndAddHeaders(req.url, rawHtml, res, next, async (html) => {
230-
const resolvedUrls = server.resolvedUrls;
231-
const baseUrl = resolvedUrls?.local[0] ?? resolvedUrls?.network[0];
232-
233-
const { content } = await renderPage({
234-
document: html,
235-
route: new URL(req.originalUrl ?? '/', baseUrl).toString(),
236-
serverContext: 'ssr',
237-
loadBundle: (uri: string) =>
238-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
239-
server.ssrLoadModule(uri.slice(1)) as any,
240-
// Files here are only needed for critical CSS inlining.
241-
outputFiles: {},
242-
// TODO: add support for critical css inlining.
243-
inlineCriticalCss: false,
244-
});
245-
246-
return indexHtmlTransformer && content ? await indexHtmlTransformer(content) : content;
247-
});
248-
}
249-
250127
if (ssr) {
251-
server.middlewares.use(angularSSRMiddleware);
128+
server.middlewares.use(
129+
createAngularSSRMiddleware(server, outputFiles, indexHtmlTransformer),
130+
);
252131
}
253132

254-
server.middlewares.use(function angularIndexMiddleware(req, res, next) {
255-
if (!req.url) {
256-
next();
257-
258-
return;
259-
}
260-
261-
// Parse the incoming request.
262-
// The base of the URL is unused but required to parse the URL.
263-
const pathname = pathnameWithoutBasePath(req.url, server.config.base);
264-
265-
if (pathname === '/' || pathname === `/index.html`) {
266-
const rawHtml = outputFiles.get('/index.html')?.contents;
267-
if (rawHtml) {
268-
transformIndexHtmlAndAddHeaders(req.url, rawHtml, res, next, indexHtmlTransformer);
269-
270-
return;
271-
}
272-
}
273-
274-
next();
275-
});
133+
server.middlewares.use(
134+
createAngularIndexHtmlMiddleware(server, outputFiles, indexHtmlTransformer),
135+
);
276136
};
277-
278-
function transformIndexHtmlAndAddHeaders(
279-
url: string,
280-
rawHtml: Uint8Array,
281-
res: ServerResponse<import('http').IncomingMessage>,
282-
next: Connect.NextFunction,
283-
additionalTransformer?: (html: string) => Promise<string | undefined>,
284-
) {
285-
server
286-
.transformIndexHtml(url, Buffer.from(rawHtml).toString('utf-8'))
287-
.then(async (processedHtml) => {
288-
if (additionalTransformer) {
289-
const content = await additionalTransformer(processedHtml);
290-
if (!content) {
291-
next();
292-
293-
return;
294-
}
295-
296-
processedHtml = content;
297-
}
298-
299-
res.setHeader('Content-Type', 'text/html');
300-
res.setHeader('Cache-Control', 'no-cache');
301-
if (extraHeaders) {
302-
Object.entries(extraHeaders).forEach(([name, value]) => res.setHeader(name, value));
303-
}
304-
res.end(processedHtml);
305-
})
306-
.catch((error) => next(error));
307-
}
308137
},
309138
};
310139
}
@@ -334,61 +163,3 @@ async function loadViteClientCode(file: string): Promise<string> {
334163

335164
return updatedContents;
336165
}
337-
338-
function pathnameWithoutBasePath(url: string, basePath: string): string {
339-
const parsedUrl = new URL(url, 'http://localhost');
340-
const pathname = decodeURIComponent(parsedUrl.pathname);
341-
342-
// slice(basePath.length - 1) to retain the trailing slash
343-
return basePath !== '/' && pathname.startsWith(basePath)
344-
? pathname.slice(basePath.length - 1)
345-
: pathname;
346-
}
347-
348-
function angularHtmlFallbackMiddleware(
349-
req: Connect.IncomingMessage,
350-
res: ServerResponse,
351-
next: Connect.NextFunction,
352-
): void {
353-
// Similar to how it is handled in vite
354-
// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/middlewares/htmlFallback.ts#L15C19-L15C45
355-
if (
356-
(req.method === 'GET' || req.method === 'HEAD') &&
357-
(!req.url || !lookupMimeTypeFromRequest(req.url)) &&
358-
(!req.headers.accept ||
359-
req.headers.accept.includes('text/html') ||
360-
req.headers.accept.includes('text/*') ||
361-
req.headers.accept.includes('*/*'))
362-
) {
363-
req.url = '/index.html';
364-
}
365-
366-
next();
367-
}
368-
369-
function lookupMimeTypeFromRequest(url: string): string | undefined {
370-
const extension = extname(url.split('?')[0]);
371-
372-
if (extension === '.ico') {
373-
return 'image/x-icon';
374-
}
375-
376-
return extension && lookupMimeType(extension);
377-
}
378-
379-
function redirect(res: ServerResponse, location: string): void {
380-
res.statusCode = 301;
381-
res.setHeader('Content-Type', 'text/html');
382-
res.setHeader('Location', location);
383-
res.end(`
384-
<!DOCTYPE html>
385-
<html lang="en">
386-
<head>
387-
<meta charset="utf-8">
388-
<title>Redirecting</title>
389-
</head>
390-
<body>
391-
<pre>Redirecting to <a href="${location}">${location}</a></pre>
392-
</body>
393-
</html>`);
394-
}

0 commit comments

Comments
 (0)