Skip to content

Commit ed95a92

Browse files
clydinalan-agius4
authored andcommitted
refactor(@angular-devkit/build-angular): move Angular memory Vite plugin into separate file
To reduce the amount of code within the main Vite development server file, the Vite plugin that handles the in-memory output file integration for the Angular build system is now within a separate file. (cherry picked from commit ebe3afb)
1 parent 85ac2f3 commit ed95a92

File tree

2 files changed

+321
-280
lines changed

2 files changed

+321
-280
lines changed

packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts

Lines changed: 14 additions & 280 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,22 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import remapping, { SourceMapInput } from '@ampproject/remapping';
109
import type { BuilderContext } from '@angular-devkit/architect';
1110
import type { json, logging } from '@angular-devkit/core';
1211
import type { Plugin } from 'esbuild';
13-
import { lookup as lookupMimeType } from 'mrmime';
1412
import assert from 'node:assert';
1513
import { readFile } from 'node:fs/promises';
16-
import { ServerResponse } from 'node:http';
17-
import { dirname, extname, join, relative } from 'node:path';
14+
import { join } from 'node:path';
1815
import type { Connect, DepOptimizationConfig, InlineConfig, ViteDevServer } from 'vite';
1916
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
2017
import { ExternalResultMetadata } from '../../tools/esbuild/bundler-execution-result';
2118
import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer';
2219
import { createRxjsEsmResolutionPlugin } from '../../tools/esbuild/rxjs-esm-resolution-plugin';
2320
import { getFeatureSupport, transformSupportedBrowsersToTargets } from '../../tools/esbuild/utils';
21+
import { createAngularMemoryPlugin } from '../../tools/vite/angular-memory-plugin';
2422
import { createAngularLocaleDataPlugin } from '../../tools/vite/i18n-locale-plugin';
2523
import { loadProxyConfiguration, normalizeSourceMaps } from '../../utils';
2624
import { loadEsmModule } from '../../utils/load-esm';
27-
import { renderPage } from '../../utils/server-rendering/render-page';
2825
import { getSupportedBrowsers } from '../../utils/supported-browsers';
2926
import { getIndexOutputFile } from '../../utils/webpack-browser-config';
3027
import { buildApplicationInternal } from '../application';
@@ -423,7 +420,6 @@ function analyzeResultFiles(
423420
}
424421
}
425422

426-
// eslint-disable-next-line max-lines-per-function
427423
export async function setupServer(
428424
serverOptions: NormalizedDevServerOptions,
429425
outputFiles: Map<string, OutputFileRecord>,
@@ -532,248 +528,18 @@ export async function setupServer(
532528
},
533529
plugins: [
534530
createAngularLocaleDataPlugin(),
535-
{
536-
name: 'vite:angular-memory',
537-
// Ensures plugin hooks run before built-in Vite hooks
538-
enforce: 'pre',
539-
async resolveId(source, importer) {
540-
// Prevent vite from resolving an explicit external dependency (`externalDependencies` option)
541-
if (externalMetadata.explicit.includes(source)) {
542-
// This is still not ideal since Vite will still transform the import specifier to
543-
// `/@id/${source}` but is currently closer to a raw external than a resolved file path.
544-
return source;
545-
}
546-
547-
if (importer && source[0] === '.' && importer.startsWith(virtualProjectRoot)) {
548-
// Remove query if present
549-
const [importerFile] = importer.split('?', 1);
550-
551-
source =
552-
'/' +
553-
normalizePath(join(dirname(relative(virtualProjectRoot, importerFile)), source));
554-
}
555-
556-
const [file] = source.split('?', 1);
557-
if (outputFiles.has(file)) {
558-
return join(virtualProjectRoot, source);
559-
}
560-
},
561-
load(id) {
562-
const [file] = id.split('?', 1);
563-
const relativeFile = '/' + normalizePath(relative(virtualProjectRoot, file));
564-
const codeContents = outputFiles.get(relativeFile)?.contents;
565-
if (codeContents === undefined) {
566-
if (relativeFile.endsWith('/node_modules/vite/dist/client/client.mjs')) {
567-
return loadViteClientCode(file);
568-
}
569-
570-
return;
571-
}
572-
573-
const code = Buffer.from(codeContents).toString('utf-8');
574-
const mapContents = outputFiles.get(relativeFile + '.map')?.contents;
575-
576-
return {
577-
// Remove source map URL comments from the code if a sourcemap is present.
578-
// Vite will inline and add an additional sourcemap URL for the sourcemap.
579-
code: mapContents ? code.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '') : code,
580-
map: mapContents && Buffer.from(mapContents).toString('utf-8'),
581-
};
582-
},
583-
configureServer(server) {
584-
const originalssrTransform = server.ssrTransform;
585-
server.ssrTransform = async (code, map, url, originalCode) => {
586-
const result = await originalssrTransform(code, null, url, originalCode);
587-
if (!result || !result.map || !map) {
588-
return result;
589-
}
590-
591-
const remappedMap = remapping(
592-
[result.map as SourceMapInput, map as SourceMapInput],
593-
() => null,
594-
);
595-
596-
// Set the sourcemap root to the workspace root. This is needed since we set a virtual path as root.
597-
remappedMap.sourceRoot = normalizePath(serverOptions.workspaceRoot) + '/';
598-
599-
return {
600-
...result,
601-
map: remappedMap as (typeof result)['map'],
602-
};
603-
};
604-
605-
// Assets and resources get handled first
606-
server.middlewares.use(function angularAssetsMiddleware(req, res, next) {
607-
if (req.url === undefined || res.writableEnded) {
608-
return;
609-
}
610-
611-
// Parse the incoming request.
612-
// The base of the URL is unused but required to parse the URL.
613-
const pathname = pathnameWithoutBasePath(req.url, server.config.base);
614-
const extension = extname(pathname);
615-
616-
// Rewrite all build assets to a vite raw fs URL
617-
const assetSourcePath = assets.get(pathname);
618-
if (assetSourcePath !== undefined) {
619-
// Workaround to disable Vite transformer middleware.
620-
// See: https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/middlewares/transform.ts#L201 and
621-
// https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/transformRequest.ts#L204-L206
622-
req.headers.accept = 'text/html';
623-
624-
// The encoding needs to match what happens in the vite static middleware.
625-
// ref: https://github.com/vitejs/vite/blob/d4f13bd81468961c8c926438e815ab6b1c82735e/packages/vite/src/node/server/middlewares/static.ts#L163
626-
req.url = `${server.config.base}@fs/${encodeURI(assetSourcePath)}`;
627-
next();
628-
629-
return;
630-
}
631-
632-
// Resource files are handled directly.
633-
// Global stylesheets (CSS files) are currently considered resources to workaround
634-
// dev server sourcemap issues with stylesheets.
635-
if (extension !== '.js' && extension !== '.html') {
636-
const outputFile = outputFiles.get(pathname);
637-
if (outputFile?.servable) {
638-
const mimeType = lookupMimeType(extension);
639-
if (mimeType) {
640-
res.setHeader('Content-Type', mimeType);
641-
}
642-
res.setHeader('Cache-Control', 'no-cache');
643-
if (serverOptions.headers) {
644-
Object.entries(serverOptions.headers).forEach(([name, value]) =>
645-
res.setHeader(name, value),
646-
);
647-
}
648-
res.end(outputFile.contents);
649-
650-
return;
651-
}
652-
}
653-
654-
next();
655-
});
656-
657-
if (extensionMiddleware?.length) {
658-
extensionMiddleware.forEach((middleware) => server.middlewares.use(middleware));
659-
}
660-
661-
// Returning a function, installs middleware after the main transform middleware but
662-
// before the built-in HTML middleware
663-
return () => {
664-
function angularSSRMiddleware(
665-
req: Connect.IncomingMessage,
666-
res: ServerResponse,
667-
next: Connect.NextFunction,
668-
) {
669-
const url = req.originalUrl;
670-
if (
671-
// Skip if path is not defined.
672-
!url ||
673-
// Skip if path is like a file.
674-
// NOTE: We use a regexp to mitigate against matching requests like: /browse/pl.0ef59752c0cd457dbf1391f08cbd936f
675-
/^\.[a-z]{2,4}$/i.test(extname(url.split('?')[0]))
676-
) {
677-
next();
678-
679-
return;
680-
}
681-
682-
const rawHtml = outputFiles.get('/index.server.html')?.contents;
683-
if (!rawHtml) {
684-
next();
685-
686-
return;
687-
}
688-
689-
transformIndexHtmlAndAddHeaders(url, rawHtml, res, next, async (html) => {
690-
const { content } = await renderPage({
691-
document: html,
692-
route: new URL(req.originalUrl ?? '/', server.resolvedUrls?.local[0]).toString(),
693-
serverContext: 'ssr',
694-
loadBundle: (uri: string) =>
695-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
696-
server.ssrLoadModule(uri.slice(1)) as any,
697-
// Files here are only needed for critical CSS inlining.
698-
outputFiles: {},
699-
// TODO: add support for critical css inlining.
700-
inlineCriticalCss: false,
701-
});
702-
703-
return indexHtmlTransformer && content
704-
? await indexHtmlTransformer(content)
705-
: content;
706-
});
707-
}
708-
709-
if (ssr) {
710-
server.middlewares.use(angularSSRMiddleware);
711-
}
712-
713-
server.middlewares.use(function angularIndexMiddleware(req, res, next) {
714-
if (!req.url) {
715-
next();
716-
717-
return;
718-
}
719-
720-
// Parse the incoming request.
721-
// The base of the URL is unused but required to parse the URL.
722-
const pathname = pathnameWithoutBasePath(req.url, server.config.base);
723-
724-
if (pathname === '/' || pathname === `/index.html`) {
725-
const rawHtml = outputFiles.get('/index.html')?.contents;
726-
if (rawHtml) {
727-
transformIndexHtmlAndAddHeaders(
728-
req.url,
729-
rawHtml,
730-
res,
731-
next,
732-
indexHtmlTransformer,
733-
);
734-
735-
return;
736-
}
737-
}
738-
739-
next();
740-
});
741-
};
742-
743-
function transformIndexHtmlAndAddHeaders(
744-
url: string,
745-
rawHtml: Uint8Array,
746-
res: ServerResponse<import('http').IncomingMessage>,
747-
next: Connect.NextFunction,
748-
additionalTransformer?: (html: string) => Promise<string | undefined>,
749-
) {
750-
server
751-
.transformIndexHtml(url, Buffer.from(rawHtml).toString('utf-8'))
752-
.then(async (processedHtml) => {
753-
if (additionalTransformer) {
754-
const content = await additionalTransformer(processedHtml);
755-
if (!content) {
756-
next();
757-
758-
return;
759-
}
760-
761-
processedHtml = content;
762-
}
763-
764-
res.setHeader('Content-Type', 'text/html');
765-
res.setHeader('Cache-Control', 'no-cache');
766-
if (serverOptions.headers) {
767-
Object.entries(serverOptions.headers).forEach(([name, value]) =>
768-
res.setHeader(name, value),
769-
);
770-
}
771-
res.end(processedHtml);
772-
})
773-
.catch((error) => next(error));
774-
}
775-
},
776-
},
531+
createAngularMemoryPlugin({
532+
workspaceRoot: serverOptions.workspaceRoot,
533+
virtualProjectRoot,
534+
outputFiles,
535+
assets,
536+
ssr,
537+
external: externalMetadata.explicit,
538+
indexHtmlTransformer,
539+
extensionMiddleware,
540+
extraHeaders: serverOptions.headers,
541+
normalizePath,
542+
}),
777543
],
778544
// Browser only optimizeDeps. (This does not run for SSR dependencies).
779545
optimizeDeps: getDepOptimizationConfig({
@@ -810,38 +576,6 @@ export async function setupServer(
810576
return configuration;
811577
}
812578

813-
/**
814-
* Reads the resolved Vite client code from disk and updates the content to remove
815-
* an unactionable suggestion to update the Vite configuration file to disable the
816-
* error overlay. The Vite configuration file is not present when used in the Angular
817-
* CLI.
818-
* @param file The absolute path to the Vite client code.
819-
* @returns
820-
*/
821-
async function loadViteClientCode(file: string) {
822-
const originalContents = await readFile(file, 'utf-8');
823-
let contents = originalContents.replace('You can also disable this overlay by setting', '');
824-
contents = contents.replace(
825-
// eslint-disable-next-line max-len
826-
'<code part="config-option-name">server.hmr.overlay</code> to <code part="config-option-value">false</code> in <code part="config-file-name">vite.config.js.</code>',
827-
'',
828-
);
829-
830-
assert(originalContents !== contents, 'Failed to update Vite client error overlay text.');
831-
832-
return contents;
833-
}
834-
835-
function pathnameWithoutBasePath(url: string, basePath: string): string {
836-
const parsedUrl = new URL(url, 'http://localhost');
837-
const pathname = decodeURIComponent(parsedUrl.pathname);
838-
839-
// slice(basePath.length - 1) to retain the trailing slash
840-
return basePath !== '/' && pathname.startsWith(basePath)
841-
? pathname.slice(basePath.length - 1)
842-
: pathname;
843-
}
844-
845579
type ViteEsBuildPlugin = NonNullable<
846580
NonNullable<DepOptimizationConfig['esbuildOptions']>['plugins']
847581
>[0];

0 commit comments

Comments
 (0)