|
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