66 * found in the LICENSE file at https://angular.io/license
77 */
88
9- import type { PartialMessage , Plugin , PluginBuild } from 'esbuild' ;
9+ import type { OnLoadResult , PartialMessage , Plugin , PluginBuild , ResolveResult } from 'esbuild' ;
10+ import assert from 'node:assert' ;
1011import { readFile } from 'node:fs/promises' ;
11- import { dirname , join , relative } from 'node:path' ;
12+ import { dirname , extname , join , relative } from 'node:path' ;
1213import { fileURLToPath , pathToFileURL } from 'node:url' ;
13- import type { CompileResult , Exception } from 'sass' ;
14+ import type { CompileResult , Exception , Syntax } from 'sass' ;
1415import {
1516 FileImporterWithRequestContextOptions ,
1617 SassWorkerImplementation ,
1718} from '../../sass/sass-service' ;
1819
20+ export interface SassPluginOptions {
21+ sourcemap : boolean ;
22+ loadPaths ?: string [ ] ;
23+ inlineComponentData ?: Record < string , string > ;
24+ }
25+
1926let sassWorkerPool : SassWorkerImplementation | undefined ;
2027
2128function isSassException ( error : unknown ) : error is Exception {
@@ -27,7 +34,7 @@ export function shutdownSassWorkerPool(): void {
2734 sassWorkerPool = undefined ;
2835}
2936
30- export function createSassPlugin ( options : { sourcemap : boolean ; loadPaths ?: string [ ] } ) : Plugin {
37+ export function createSassPlugin ( options : SassPluginOptions ) : Plugin {
3138 return {
3239 name : 'angular-sass' ,
3340 setup ( build : PluginBuild ) : void {
@@ -55,105 +62,123 @@ export function createSassPlugin(options: { sourcemap: boolean; loadPaths?: stri
5562 return result ;
5663 } ;
5764
58- build . onLoad ( { filter : / \. s [ a c ] s s $ / } , async ( args ) => {
59- // Lazily load Sass when a Sass file is found
60- sassWorkerPool ??= new SassWorkerImplementation ( true ) ;
61-
62- const warnings : PartialMessage [ ] = [ ] ;
63- try {
64- const data = await readFile ( args . path , 'utf-8' ) ;
65- const { css, sourceMap, loadedUrls } = await sassWorkerPool . compileStringAsync ( data , {
66- url : pathToFileURL ( args . path ) ,
67- style : 'expanded' ,
68- loadPaths : options . loadPaths ,
69- sourceMap : options . sourcemap ,
70- sourceMapIncludeSources : options . sourcemap ,
71- quietDeps : true ,
72- importers : [
73- {
74- findFileUrl : async (
75- url ,
76- { previousResolvedModules } : FileImporterWithRequestContextOptions ,
77- ) : Promise < URL | null > => {
78- const result = await resolveUrl ( url , previousResolvedModules ) ;
79-
80- // Check for package deep imports
81- if ( ! result . path ) {
82- const parts = url . split ( '/' ) ;
83- const hasScope = parts . length >= 2 && parts [ 0 ] . startsWith ( '@' ) ;
84- const [ nameOrScope , nameOrFirstPath , ...pathPart ] = parts ;
85- const packageName = hasScope
86- ? `${ nameOrScope } /${ nameOrFirstPath } `
87- : nameOrScope ;
88-
89- const packageResult = await resolveUrl (
90- packageName + '/package.json' ,
91- previousResolvedModules ,
92- ) ;
93-
94- if ( packageResult . path ) {
95- return pathToFileURL (
96- join (
97- dirname ( packageResult . path ) ,
98- ! hasScope ? nameOrFirstPath : '' ,
99- ...pathPart ,
100- ) ,
101- ) ;
102- }
103- }
104-
105- return result . path ? pathToFileURL ( result . path ) : null ;
106- } ,
107- } ,
108- ] ,
109- logger : {
110- warn : ( text , { deprecation, span } ) => {
111- warnings . push ( {
112- text : deprecation ? 'Deprecation' : text ,
113- location : span && {
114- file : span . url && fileURLToPath ( span . url ) ,
115- lineText : span . context ,
116- // Sass line numbers are 0-based while esbuild's are 1-based
117- line : span . start . line + 1 ,
118- column : span . start . column ,
119- } ,
120- notes : deprecation ? [ { text } ] : undefined ,
121- } ) ;
122- } ,
123- } ,
124- } ) ;
65+ build . onLoad (
66+ { filter : / ^ a n g u l a r : s t y l e s \/ c o m p o n e n t ; s [ a c ] s s ; / , namespace : 'angular:styles/component' } ,
67+ async ( args ) => {
68+ const data = options . inlineComponentData ?. [ args . path ] ;
69+ assert ( data , `component style name should always be found [${ args . path } ]` ) ;
12570
126- return {
127- loader : 'css' ,
128- contents : sourceMap
129- ? `${ css } \n${ sourceMapToUrlComment ( sourceMap , dirname ( args . path ) ) } `
130- : css ,
131- watchFiles : loadedUrls . map ( ( url ) => fileURLToPath ( url ) ) ,
132- warnings,
133- } ;
134- } catch ( error ) {
135- if ( isSassException ( error ) ) {
136- const file = error . span . url ? fileURLToPath ( error . span . url ) : undefined ;
137-
138- return {
139- loader : 'css' ,
140- errors : [
141- {
142- text : error . message ,
143- } ,
144- ] ,
145- warnings,
146- watchFiles : file ? [ file ] : undefined ,
147- } ;
148- }
71+ const [ , language , , filePath ] = args . path . split ( ';' , 4 ) ;
72+ const syntax = language === 'sass' ? 'indented' : 'scss' ;
14973
150- throw error ;
151- }
74+ return compileString ( data , filePath , syntax , options , resolveUrl ) ;
75+ } ,
76+ ) ;
77+
78+ build . onLoad ( { filter : / \. s [ a c ] s s $ / } , async ( args ) => {
79+ const data = await readFile ( args . path , 'utf-8' ) ;
80+ const syntax = extname ( args . path ) . toLowerCase ( ) === '.sass' ? 'indented' : 'scss' ;
81+
82+ return compileString ( data , args . path , syntax , options , resolveUrl ) ;
15283 } ) ;
15384 } ,
15485 } ;
15586}
15687
88+ async function compileString (
89+ data : string ,
90+ filePath : string ,
91+ syntax : Syntax ,
92+ options : SassPluginOptions ,
93+ resolveUrl : ( url : string , previousResolvedModules ?: Set < string > ) => Promise < ResolveResult > ,
94+ ) : Promise < OnLoadResult > {
95+ // Lazily load Sass when a Sass file is found
96+ sassWorkerPool ??= new SassWorkerImplementation ( true ) ;
97+
98+ const warnings : PartialMessage [ ] = [ ] ;
99+ try {
100+ const { css, sourceMap, loadedUrls } = await sassWorkerPool . compileStringAsync ( data , {
101+ url : pathToFileURL ( filePath ) ,
102+ style : 'expanded' ,
103+ syntax,
104+ loadPaths : options . loadPaths ,
105+ sourceMap : options . sourcemap ,
106+ sourceMapIncludeSources : options . sourcemap ,
107+ quietDeps : true ,
108+ importers : [
109+ {
110+ findFileUrl : async (
111+ url ,
112+ { previousResolvedModules } : FileImporterWithRequestContextOptions ,
113+ ) : Promise < URL | null > => {
114+ const result = await resolveUrl ( url , previousResolvedModules ) ;
115+
116+ // Check for package deep imports
117+ if ( ! result . path ) {
118+ const parts = url . split ( '/' ) ;
119+ const hasScope = parts . length >= 2 && parts [ 0 ] . startsWith ( '@' ) ;
120+ const [ nameOrScope , nameOrFirstPath , ...pathPart ] = parts ;
121+ const packageName = hasScope ? `${ nameOrScope } /${ nameOrFirstPath } ` : nameOrScope ;
122+
123+ const packageResult = await resolveUrl (
124+ packageName + '/package.json' ,
125+ previousResolvedModules ,
126+ ) ;
127+
128+ if ( packageResult . path ) {
129+ return pathToFileURL (
130+ join ( dirname ( packageResult . path ) , ! hasScope ? nameOrFirstPath : '' , ...pathPart ) ,
131+ ) ;
132+ }
133+ }
134+
135+ return result . path ? pathToFileURL ( result . path ) : null ;
136+ } ,
137+ } ,
138+ ] ,
139+ logger : {
140+ warn : ( text , { deprecation, span } ) => {
141+ warnings . push ( {
142+ text : deprecation ? 'Deprecation' : text ,
143+ location : span && {
144+ file : span . url && fileURLToPath ( span . url ) ,
145+ lineText : span . context ,
146+ // Sass line numbers are 0-based while esbuild's are 1-based
147+ line : span . start . line + 1 ,
148+ column : span . start . column ,
149+ } ,
150+ notes : deprecation ? [ { text } ] : undefined ,
151+ } ) ;
152+ } ,
153+ } ,
154+ } ) ;
155+
156+ return {
157+ loader : 'css' ,
158+ contents : sourceMap ? `${ css } \n${ sourceMapToUrlComment ( sourceMap , dirname ( filePath ) ) } ` : css ,
159+ watchFiles : loadedUrls . map ( ( url ) => fileURLToPath ( url ) ) ,
160+ warnings,
161+ } ;
162+ } catch ( error ) {
163+ if ( isSassException ( error ) ) {
164+ const file = error . span . url ? fileURLToPath ( error . span . url ) : undefined ;
165+
166+ return {
167+ loader : 'css' ,
168+ errors : [
169+ {
170+ text : error . message ,
171+ } ,
172+ ] ,
173+ warnings,
174+ watchFiles : file ? [ file ] : undefined ,
175+ } ;
176+ }
177+
178+ throw error ;
179+ }
180+ }
181+
157182function sourceMapToUrlComment (
158183 sourceMap : Exclude < CompileResult [ 'sourceMap' ] , undefined > ,
159184 root : string ,
0 commit comments