diff --git a/packages/angular/build/src/builders/application/setup-bundling.ts b/packages/angular/build/src/builders/application/setup-bundling.ts index 498d72ffa5b5..45bac12e6466 100644 --- a/packages/angular/build/src/builders/application/setup-bundling.ts +++ b/packages/angular/build/src/builders/application/setup-bundling.ts @@ -93,16 +93,7 @@ export function setupBundlerContexts( new BundlerContext( workspaceRoot, !!options.watch, - createServerCodeBundleOptions( - { - ...options, - // Disable external deps for server bundles. - // This is because it breaks Vite 'optimizeDeps' for SSR. - externalPackages: false, - }, - nodeTargets, - codeBundleCache, - ), + createServerCodeBundleOptions(options, nodeTargets, codeBundleCache), ), ); diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index d0f87c736793..e4b4e655f66d 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -10,6 +10,7 @@ import type { BuilderContext } from '@angular-devkit/architect'; import type { Plugin } from 'esbuild'; import assert from 'node:assert'; import { readFile } from 'node:fs/promises'; +import { builtinModules } from 'node:module'; import { basename, join } from 'node:path'; import type { Connect, DepOptimizationConfig, InlineConfig, ViteDevServer } from 'vite'; import { createAngularMemoryPlugin } from '../../tools/vite/angular-memory-plugin'; @@ -205,21 +206,31 @@ export async function* serveWithVite( } // To avoid disconnecting the array objects from the option, these arrays need to be mutated instead of replaced. + let requiresServerRestart = false; if (result.externalMetadata) { const { implicitBrowser, implicitServer, explicit } = result.externalMetadata; + const implicitServerFiltered = (implicitServer as string[]).filter( + (m) => removeNodeJsBuiltinModules(m) && removeAbsoluteUrls(m), + ); + const implicitBrowserFiltered = (implicitBrowser as string[]).filter(removeAbsoluteUrls); + + if (browserOptions.ssr && serverOptions.prebundle !== false) { + const previousImplicitServer = new Set(externalMetadata.implicitServer); + // Restart the server to force SSR dep re-optimization when a dependency has been added. + // This is a workaround for: https://github.com/vitejs/vite/issues/14896 + requiresServerRestart = implicitServerFiltered.some( + (dep) => !previousImplicitServer.has(dep), + ); + } + // Empty Arrays to avoid growing unlimited with every re-build. externalMetadata.explicit.length = 0; externalMetadata.implicitServer.length = 0; externalMetadata.implicitBrowser.length = 0; externalMetadata.explicit.push(...explicit); - // Remove any absolute URLs (http://, https://, //) to avoid Vite's prebundling from processing them as files - externalMetadata.implicitServer.push( - ...(implicitServer as string[]).filter((value) => !/^(?:https?:)?\/\//.test(value)), - ); - externalMetadata.implicitBrowser.push( - ...(implicitBrowser as string[]).filter((value) => !/^(?:https?:)?\/\//.test(value)), - ); + externalMetadata.implicitServer.push(...implicitServerFiltered); + externalMetadata.implicitBrowser.push(...implicitBrowserFiltered); // The below needs to be sorted as Vite uses these options are part of the hashing invalidation algorithm. // See: https://github.com/vitejs/vite/blob/0873bae0cfe0f0718ad2f5743dd34a17e4ab563d/packages/vite/src/node/optimizer/index.ts#L1203-L1239 @@ -234,7 +245,13 @@ export async function* serveWithVite( ...new Set([...server.config.server.fs.allow, ...assetFiles.values()]), ]; - handleUpdate(normalizePath, generatedFiles, server, serverOptions, context.logger); + await handleUpdate(normalizePath, generatedFiles, server, serverOptions, context.logger); + + if (requiresServerRestart) { + // Restart the server to force SSR dep re-optimization when a dependency has been added. + // This is a workaround for: https://github.com/vitejs/vite/issues/14896 + await server.restart(); + } } else { const projectName = context.target?.project; if (!projectName) { @@ -275,16 +292,10 @@ export async function* serveWithVite( server = await createServer(serverConfiguration); await server.listen(); - if (serverConfiguration.ssr?.optimizeDeps?.disabled === false) { - /** - * Vite will only start dependency optimization of SSR modules when the first request comes in. - * In some cases, this causes a long waiting time. To mitigate this, we call `ssrLoadModule` to - * initiate this process before the first request. - * - * NOTE: This will intentionally fail from the unknown module, but currently there is no other way - * to initiate the SSR dep optimizer. - */ - void server.ssrLoadModule('').catch(() => {}); + if (browserOptions.ssr && serverOptions.prebundle !== false) { + // Warm up the SSR request and begin optimizing dependencies. + // Without this, Vite will only start optimizing SSR modules when the first request is made. + void server.warmupRequest('./main.server.mjs', { ssr: true }); } const urls = server.resolvedUrls; @@ -469,11 +480,6 @@ export async function setupServer( join(serverOptions.workspaceRoot, `.angular/vite-root`, serverOptions.buildTarget.project), ); - const serverExplicitExternal = [ - ...(await import('node:module')).builtinModules, - ...externalMetadata.explicit, - ]; - const cacheDir = join(serverOptions.cacheOptions.path, 'vite'); const configuration: InlineConfig = { configFile: false, @@ -536,23 +542,12 @@ export async function setupServer( // Note: `true` and `/.*/` have different sematics. When true, the `external` option is ignored. noExternal: /.*/, // Exclude any Node.js built in module and provided dependencies (currently build defined externals) - external: serverExplicitExternal, + external: externalMetadata.explicit, optimizeDeps: getDepOptimizationConfig({ - /** - * ********************************************* - * NOTE: Temporary disable 'optimizeDeps' for SSR. - * ********************************************* - * - * Currently this causes a number of issues. - * - Deps are re-optimized everytime the server is started. - * - Added deps after a rebuild are not optimized. - * - Breaks RxJs (Unless it is added as external). See: https://github.com/angular/angular-cli/issues/26235 - */ - // Only enable with caching since it causes prebundle dependencies to be cached - disabled: true, // serverOptions.prebundle === false, + disabled: serverOptions.prebundle === false, // Exclude any explicitly defined dependencies (currently build defined externals and node.js built-ins) - exclude: serverExplicitExternal, + exclude: externalMetadata.explicit, // Include all implict dependencies from the external packages internal option include: externalMetadata.implicitServer, ssr: true, @@ -678,3 +673,14 @@ function getDepOptimizationConfig({ }, }; } + +const nodeJsBuiltinModules = new Set(builtinModules); +/** Remove any Node.js builtin modules to avoid Vite's prebundling from processing them as files. */ +function removeNodeJsBuiltinModules(value: string): boolean { + return !nodeJsBuiltinModules.has(value); +} + +/** Remove any absolute URLs (http://, https://, //) to avoid Vite's prebundling from processing them as files. */ +function removeAbsoluteUrls(value: string): boolean { + return !/^(?:https?:)?\/\//.test(value); +}