|
6 | 6 | * found in the LICENSE file at https://angular.io/license
|
7 | 7 | */
|
8 | 8 |
|
9 |
| -import remapping, { SourceMapInput } from '@ampproject/remapping'; |
10 | 9 | import type { BuilderContext } from '@angular-devkit/architect';
|
11 | 10 | import type { json, logging } from '@angular-devkit/core';
|
12 | 11 | import type { Plugin } from 'esbuild';
|
13 |
| -import { lookup as lookupMimeType } from 'mrmime'; |
14 | 12 | import assert from 'node:assert';
|
15 | 13 | 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'; |
18 | 15 | import type { Connect, DepOptimizationConfig, InlineConfig, ViteDevServer } from 'vite';
|
19 | 16 | import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
|
20 | 17 | import { ExternalResultMetadata } from '../../tools/esbuild/bundler-execution-result';
|
21 | 18 | import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer';
|
22 | 19 | import { createRxjsEsmResolutionPlugin } from '../../tools/esbuild/rxjs-esm-resolution-plugin';
|
23 | 20 | import { getFeatureSupport, transformSupportedBrowsersToTargets } from '../../tools/esbuild/utils';
|
| 21 | +import { createAngularMemoryPlugin } from '../../tools/vite/angular-memory-plugin'; |
24 | 22 | import { createAngularLocaleDataPlugin } from '../../tools/vite/i18n-locale-plugin';
|
25 | 23 | import { loadProxyConfiguration, normalizeSourceMaps } from '../../utils';
|
26 | 24 | import { loadEsmModule } from '../../utils/load-esm';
|
27 |
| -import { renderPage } from '../../utils/server-rendering/render-page'; |
28 | 25 | import { getSupportedBrowsers } from '../../utils/supported-browsers';
|
29 | 26 | import { getIndexOutputFile } from '../../utils/webpack-browser-config';
|
30 | 27 | import { buildApplicationInternal } from '../application';
|
@@ -423,7 +420,6 @@ function analyzeResultFiles(
|
423 | 420 | }
|
424 | 421 | }
|
425 | 422 |
|
426 |
| -// eslint-disable-next-line max-lines-per-function |
427 | 423 | export async function setupServer(
|
428 | 424 | serverOptions: NormalizedDevServerOptions,
|
429 | 425 | outputFiles: Map<string, OutputFileRecord>,
|
@@ -532,248 +528,18 @@ export async function setupServer(
|
532 | 528 | },
|
533 | 529 | plugins: [
|
534 | 530 | 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 | + }), |
777 | 543 | ],
|
778 | 544 | // Browser only optimizeDeps. (This does not run for SSR dependencies).
|
779 | 545 | optimizeDeps: getDepOptimizationConfig({
|
@@ -810,38 +576,6 @@ export async function setupServer(
|
810 | 576 | return configuration;
|
811 | 577 | }
|
812 | 578 |
|
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 |
| - |
845 | 579 | type ViteEsBuildPlugin = NonNullable<
|
846 | 580 | NonNullable<DepOptimizationConfig['esbuildOptions']>['plugins']
|
847 | 581 | >[0];
|
|
0 commit comments