@@ -43,6 +43,8 @@ export const NAMESPACE_URIS: {[ns: string]: string} = {
4343} ;
4444
4545const COMPONENT_REGEX = / % C O M P % / g;
46+ const SOURCEMAP_URL_REGEXP = / \/ \* # \s * s o u r c e M a p p i n g U R L = ( .+ ?) \s * \* \/ / ;
47+ const PROTOCOL_REGEXP = / ^ h t t p s ? : / ;
4648
4749export const COMPONENT_VARIABLE = '%COMP%' ;
4850export const HOST_ATTR = `_nghost-${ COMPONENT_VARIABLE } ` ;
@@ -80,6 +82,52 @@ export function shimStylesContent(compId: string, styles: string[]): string[] {
8082 return styles . map ( ( s ) => s . replace ( COMPONENT_REGEX , compId ) ) ;
8183}
8284
85+ /**
86+ * Prepends a baseHref to the `sourceMappingURL` within the provided CSS content.
87+ * If the `sourceMappingURL` contains an inline (encoded) map, the function skips processing.
88+ *
89+ * @note For inline stylesheets, the `sourceMappingURL` is relative to the page's origin
90+ * and not the provided baseHref. This function is needed as when accessing the page with a URL
91+ * containing two or more segments.
92+ * For example, if the baseHref is set to `/`, and you visit a URL like `http://localhost/foo/bar`,
93+ * the map would be requested from `http://localhost/foo/bar/comp.css.map` instead of what you'd expect,
94+ * which is `http://localhost/comp.css.map`. This behavior is corrected by modifying the `sourceMappingURL`
95+ * to ensure external source maps are loaded relative to the baseHref.
96+ *
97+
98+ * @param baseHref - The base URL to prepend to the `sourceMappingURL`.
99+ * @param styles - An array of CSS content strings, each potentially containing a `sourceMappingURL`.
100+ * @returns The updated array of CSS content strings with modified `sourceMappingURL` values,
101+ * or the original content if no modification is needed.
102+ */
103+ export function addBaseHrefToCssSourceMap ( baseHref : string , styles : string [ ] ) : string [ ] {
104+ if ( ! baseHref ) {
105+ return styles ;
106+ }
107+
108+ const absoluteBaseHrefUrl = new URL ( baseHref , 'http://localhost' ) ;
109+
110+ return styles . map ( ( cssContent ) => {
111+ if ( ! cssContent . includes ( 'sourceMappingURL=' ) ) {
112+ return cssContent ;
113+ }
114+
115+ return cssContent . replace ( SOURCEMAP_URL_REGEXP , ( _ , sourceMapUrl ) => {
116+ if (
117+ sourceMapUrl [ 0 ] === '/' ||
118+ sourceMapUrl . startsWith ( 'data:' ) ||
119+ PROTOCOL_REGEXP . test ( sourceMapUrl )
120+ ) {
121+ return `/*# sourceMappingURL=${ sourceMapUrl } */` ;
122+ }
123+
124+ const { pathname : resolvedSourceMapUrl } = new URL ( sourceMapUrl , absoluteBaseHrefUrl ) ;
125+
126+ return `/*# sourceMappingURL=${ resolvedSourceMapUrl } */` ;
127+ } ) ;
128+ } ) ;
129+ }
130+
83131@Injectable ( )
84132export class DomRendererFactory2 implements RendererFactory2 , OnDestroy {
85133 private readonly rendererByCompId = new Map <
@@ -145,6 +193,7 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy {
145193 const sharedStylesHost = this . sharedStylesHost ;
146194 const removeStylesOnCompDestroy = this . removeStylesOnCompDestroy ;
147195 const platformIsServer = this . platformIsServer ;
196+ const tracingService = this . tracingService ;
148197
149198 switch ( type . encapsulation ) {
150199 case ViewEncapsulation . Emulated :
@@ -157,7 +206,7 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy {
157206 doc ,
158207 ngZone ,
159208 platformIsServer ,
160- this . tracingService ,
209+ tracingService ,
161210 ) ;
162211 break ;
163212 case ViewEncapsulation . ShadowDom :
@@ -170,7 +219,7 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy {
170219 ngZone ,
171220 this . nonce ,
172221 platformIsServer ,
173- this . tracingService ,
222+ tracingService ,
174223 ) ;
175224 default :
176225 renderer = new NoneEncapsulationDomRenderer (
@@ -181,7 +230,7 @@ export class DomRendererFactory2 implements RendererFactory2, OnDestroy {
181230 doc ,
182231 ngZone ,
183232 platformIsServer ,
184- this . tracingService ,
233+ tracingService ,
185234 ) ;
186235 break ;
187236 }
@@ -449,9 +498,15 @@ class ShadowDomRenderer extends DefaultDomRenderer2 {
449498 ) {
450499 super ( eventManager , doc , ngZone , platformIsServer , tracingService ) ;
451500 this . shadowRoot = ( hostEl as any ) . attachShadow ( { mode : 'open' } ) ;
452-
453501 this . sharedStylesHost . addHost ( this . shadowRoot ) ;
454- const styles = shimStylesContent ( component . id , component . styles ) ;
502+ let styles = component . styles ;
503+ if ( ngDevMode ) {
504+ // We only do this in development, as for production users should not add CSS sourcemaps to components.
505+ const baseHref = getDOM ( ) . getBaseHref ( doc ) ?? '' ;
506+ styles = addBaseHrefToCssSourceMap ( baseHref , styles ) ;
507+ }
508+
509+ styles = shimStylesContent ( component . id , styles ) ;
455510
456511 for ( const style of styles ) {
457512 const styleEl = document . createElement ( 'style' ) ;
@@ -520,7 +575,14 @@ class NoneEncapsulationDomRenderer extends DefaultDomRenderer2 {
520575 compId ?: string ,
521576 ) {
522577 super ( eventManager , doc , ngZone , platformIsServer , tracingService ) ;
523- this . styles = compId ? shimStylesContent ( compId , component . styles ) : component . styles ;
578+ let styles = component . styles ;
579+ if ( ngDevMode ) {
580+ // We only do this in development, as for production users should not add CSS sourcemaps to components.
581+ const baseHref = getDOM ( ) . getBaseHref ( doc ) ?? '' ;
582+ styles = addBaseHrefToCssSourceMap ( baseHref , styles ) ;
583+ }
584+
585+ this . styles = compId ? shimStylesContent ( compId , styles ) : styles ;
524586 this . styleUrls = component . getExternalStyles ?.( compId ) ;
525587 }
526588
0 commit comments