From 1ff6e4f50d929addac11fe0ee9aefb5c1fab5b41 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 1 Nov 2024 11:21:53 -0400 Subject: [PATCH 1/2] refactor(@angular/build): send error status code on invalid external style component ID If an invalid component identifier is provided to the development server for an external stylesheet requiring encapsulation, both a console message and a 400 status response will now be sent. This removes the potential for invalid styles to be sent to the browser. --- .../vite/middlewares/assets-middleware.ts | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts index e1c4db401bb2..9d20c128f51f 100644 --- a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts @@ -98,26 +98,31 @@ export function createAngularAssetsMiddleware( // Shim the stylesheet if a component ID is provided if (componentId.length > 0) { // Validate component ID - if (/^[_.\-\p{Letter}\d]+-c\d+$/u.test(componentId)) { - loadEsmModule('@angular/compiler') - .then((compilerModule) => { - const encapsulatedData = compilerModule.encapsulateStyle( - new TextDecoder().decode(data), - componentId, - ); - - res.setHeader('Content-Type', 'text/css'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('ETag', etag); - res.end(encapsulatedData); - }) - .catch((e) => next(e)); + if (!/^[_.\-\p{Letter}\d]+-c\d+$/u.test(componentId)) { + const message = 'Invalid component stylesheet ID request: ' + componentId; + // eslint-disable-next-line no-console + console.error(message); + res.statusCode = 400; + res.end(message); return; - } else { - // eslint-disable-next-line no-console - console.error('Invalid component stylesheet ID request: ' + componentId); } + + loadEsmModule('@angular/compiler') + .then((compilerModule) => { + const encapsulatedData = compilerModule.encapsulateStyle( + new TextDecoder().decode(data), + componentId, + ); + + res.setHeader('Content-Type', 'text/css'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('ETag', etag); + res.end(encapsulatedData); + }) + .catch((e) => next(e)); + + return; } } } From e0600e10989033c9c050b48408cb46e124a1d518 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:18:32 -0400 Subject: [PATCH 2/2] refactor(@angular/build): avoid need to pre-import angular compiler package in dev server Within the development server, the external stylesheet encapsulation logic has been adjusted to avoid needing to asynchronously import the `@angular/compiler` package within the request execution. This removes the need to pre-import the package at the start of the development server which was previously used to avoid a delay in stylesheet response on first use. The external stylesheet response execution is also now fully synchronous. --- .../src/builders/dev-server/vite-server.ts | 5 ---- .../vite/middlewares/assets-middleware.ts | 25 +++++++------------ .../vite/plugins/setup-middlewares-plugin.ts | 23 +++++++++++++++-- 3 files changed, 30 insertions(+), 23 deletions(-) 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 a7a5d5a27e1a..3223a9280fc2 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -139,11 +139,6 @@ export async function* serveWithVite( // Enable to support component style hot reloading (`NG_HMR_CSTYLES=0` can be used to disable) browserOptions.externalRuntimeStyles = !!serverOptions.liveReload && useComponentStyleHmr; - if (browserOptions.externalRuntimeStyles) { - // Preload the @angular/compiler package to avoid first stylesheet request delays. - // Once @angular/build is native ESM, this should be re-evaluated. - void loadEsmModule('@angular/compiler'); - } // Enable to support component template hot replacement (`NG_HMR_TEMPLATE=1` can be used to enable) browserOptions.templateUpdates = !!serverOptions.liveReload && useComponentTemplateHmr; diff --git a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts index 9d20c128f51f..aefb18df229b 100644 --- a/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts +++ b/packages/angular/build/src/tools/vite/middlewares/assets-middleware.ts @@ -9,7 +9,6 @@ import { lookup as lookupMimeType } from 'mrmime'; import { extname } from 'node:path'; import type { Connect, ViteDevServer } from 'vite'; -import { loadEsmModule } from '../../../utils/load-esm'; import { AngularMemoryOutputFiles, pathnameWithoutBasePath } from '../utils'; export function createAngularAssetsMiddleware( @@ -17,6 +16,7 @@ export function createAngularAssetsMiddleware( assets: Map, outputFiles: AngularMemoryOutputFiles, usedComponentStyles: Map>, + encapsulateStyle: (style: Uint8Array, componentId: string) => string, ): Connect.NextHandleFunction { return function angularAssetsMiddleware(req, res, next) { if (req.url === undefined || res.writableEnded) { @@ -73,7 +73,7 @@ export function createAngularAssetsMiddleware( if (extension !== '.js' && extension !== '.html') { const outputFile = outputFiles.get(pathname); if (outputFile?.servable) { - const data = outputFile.contents; + let data: Uint8Array | string = outputFile.contents; if (extension === '.css') { // Inject component ID for view encapsulation if requested const componentId = new URL(req.url, 'http://localhost').searchParams.get('ngcomp'); @@ -108,22 +108,15 @@ export function createAngularAssetsMiddleware( return; } - loadEsmModule('@angular/compiler') - .then((compilerModule) => { - const encapsulatedData = compilerModule.encapsulateStyle( - new TextDecoder().decode(data), - componentId, - ); + data = encapsulateStyle(data, componentId); + } - res.setHeader('Content-Type', 'text/css'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('ETag', etag); - res.end(encapsulatedData); - }) - .catch((e) => next(e)); + res.setHeader('Content-Type', 'text/css'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('ETag', etag); + res.end(data); - return; - } + return; } } diff --git a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts index 0b3c998886e2..07983c011878 100644 --- a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts @@ -7,6 +7,7 @@ */ import type { Connect, Plugin } from 'vite'; +import { loadEsmModule } from '../../../utils/load-esm'; import { angularHtmlFallbackMiddleware, createAngularAssetsMiddleware, @@ -53,13 +54,25 @@ interface AngularSetupMiddlewaresPluginOptions { ssrMode: ServerSsrMode; } +async function createEncapsulateStyle(): Promise< + (style: Uint8Array, componentId: string) => string +> { + const { encapsulateStyle } = + await loadEsmModule('@angular/compiler'); + const decoder = new TextDecoder('utf-8'); + + return (style, componentId) => { + return encapsulateStyle(decoder.decode(style), componentId); + }; +} + export function createAngularSetupMiddlewaresPlugin( options: AngularSetupMiddlewaresPluginOptions, ): Plugin { return { name: 'vite:angular-setup-middlewares', enforce: 'pre', - configureServer(server) { + async configureServer(server) { const { indexHtmlTransformer, outputFiles, @@ -74,7 +87,13 @@ export function createAngularSetupMiddlewaresPlugin( server.middlewares.use(createAngularHeadersMiddleware(server)); server.middlewares.use(createAngularComponentMiddleware(templateUpdates)); server.middlewares.use( - createAngularAssetsMiddleware(server, assets, outputFiles, usedComponentStyles), + createAngularAssetsMiddleware( + server, + assets, + outputFiles, + usedComponentStyles, + await createEncapsulateStyle(), + ), ); extensionMiddleware?.forEach((middleware) => server.middlewares.use(middleware));