Skip to content

Commit b5af8b5

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.
1 parent 9a1c059 commit b5af8b5

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)