77 */
88
99import {
10- LegacyAsyncImporter as AsyncImporter ,
11- LegacyResult as CompileResult ,
12- LegacyException as Exception ,
13- LegacyImporterResult as ImporterResult ,
14- LegacyImporterThis as ImporterThis ,
15- LegacyOptions as Options ,
16- LegacySyncImporter as SyncImporter ,
10+ CompileResult ,
11+ Exception ,
12+ FileImporter ,
13+ Importer ,
14+ StringOptionsWithImporter ,
15+ StringOptionsWithoutImporter ,
1716} from 'sass' ;
17+ import { fileURLToPath , pathToFileURL } from 'url' ;
1818import { MessageChannel , Worker } from 'worker_threads' ;
1919import { maxWorkers } from '../utils/environment-options' ;
2020
@@ -28,23 +28,34 @@ const MAX_RENDER_WORKERS = maxWorkers;
2828 */
2929type RenderCallback = ( error ?: Exception , result ?: CompileResult ) => void ;
3030
31+ type FileImporterOptions = Parameters < FileImporter [ 'findFileUrl' ] > [ 1 ] ;
32+
3133/**
3234 * An object containing the contextual information for a specific render request.
3335 */
3436interface RenderRequest {
3537 id : number ;
3638 workerIndex : number ;
3739 callback : RenderCallback ;
38- importers ?: ( SyncImporter | AsyncImporter ) [ ] ;
40+ importers ?: Importers [ ] ;
3941}
4042
43+ /**
44+ * All available importer types.
45+ */
46+ type Importers =
47+ | Importer < 'sync' >
48+ | Importer < 'async' >
49+ | FileImporter < 'sync' >
50+ | FileImporter < 'async' > ;
51+
4152/**
4253 * A response from the Sass render Worker containing the result of the operation.
4354 */
4455interface RenderResponseMessage {
4556 id : number ;
4657 error ?: Exception ;
47- result ?: CompileResult ;
58+ result ?: Omit < CompileResult , 'loadedUrls' > & { loadedUrls : string [ ] } ;
4859}
4960
5061/**
@@ -71,46 +82,77 @@ export class SassWorkerImplementation {
7182 /**
7283 * The synchronous render function is not used by the `sass-loader`.
7384 */
74- renderSync ( ) : never {
75- throw new Error ( 'Sass renderSync is not supported.' ) ;
85+ compileString ( ) : never {
86+ throw new Error ( 'Sass compileString is not supported.' ) ;
7687 }
7788
7889 /**
7990 * Asynchronously request a Sass stylesheet to be renderered.
8091 *
92+ * @param source The contents to compile.
8193 * @param options The `dart-sass` options to use when rendering the stylesheet.
82- * @param callback The function to execute when the rendering is complete.
8394 */
84- render ( options : Options < 'async' > , callback : RenderCallback ) : void {
95+ compileStringAsync (
96+ source : string ,
97+ options : StringOptionsWithImporter < 'async' > | StringOptionsWithoutImporter < 'async' > ,
98+ ) : Promise < CompileResult > {
8599 // The `functions`, `logger` and `importer` options are JavaScript functions that cannot be transferred.
86100 // If any additional function options are added in the future, they must be excluded as well.
87- const { functions, importer , logger, ...serializableOptions } = options ;
101+ const { functions, importers , url , logger, ...serializableOptions } = options ;
88102
89103 // The CLI's configuration does not use or expose the ability to defined custom Sass functions
90104 if ( functions && Object . keys ( functions ) . length > 0 ) {
91105 throw new Error ( 'Sass custom functions are not supported.' ) ;
92106 }
93107
94- let workerIndex = this . availableWorkers . pop ( ) ;
95- if ( workerIndex === undefined ) {
96- if ( this . workers . length < MAX_RENDER_WORKERS ) {
97- workerIndex = this . workers . length ;
98- this . workers . push ( this . createWorker ( ) ) ;
99- } else {
100- workerIndex = this . nextWorkerIndex ++ ;
101- if ( this . nextWorkerIndex >= this . workers . length ) {
102- this . nextWorkerIndex = 0 ;
108+ return new Promise < CompileResult > ( ( resolve , reject ) => {
109+ let workerIndex = this . availableWorkers . pop ( ) ;
110+ if ( workerIndex === undefined ) {
111+ if ( this . workers . length < MAX_RENDER_WORKERS ) {
112+ workerIndex = this . workers . length ;
113+ this . workers . push ( this . createWorker ( ) ) ;
114+ } else {
115+ workerIndex = this . nextWorkerIndex ++ ;
116+ if ( this . nextWorkerIndex >= this . workers . length ) {
117+ this . nextWorkerIndex = 0 ;
118+ }
103119 }
104120 }
105- }
106121
107- const request = this . createRequest ( workerIndex , callback , importer ) ;
108- this . requests . set ( request . id , request ) ;
122+ const callback : RenderCallback = ( error , result ) => {
123+ if ( error ) {
124+ const url = error ?. span . url as string | undefined ;
125+ if ( url ) {
126+ error . span . url = pathToFileURL ( url ) ;
127+ }
109128
110- this . workers [ workerIndex ] . postMessage ( {
111- id : request . id ,
112- hasImporter : ! ! importer ,
113- options : serializableOptions ,
129+ reject ( error ) ;
130+
131+ return ;
132+ }
133+
134+ if ( ! result ) {
135+ reject ( 'No result.' ) ;
136+
137+ return ;
138+ }
139+
140+ resolve ( result ) ;
141+ } ;
142+
143+ const request = this . createRequest ( workerIndex , callback , importers ) ;
144+ this . requests . set ( request . id , request ) ;
145+
146+ this . workers [ workerIndex ] . postMessage ( {
147+ id : request . id ,
148+ source,
149+ hasImporter : ! ! importers ?. length ,
150+ options : {
151+ ...serializableOptions ,
152+ // URL is not serializable so to convert to string here and back to URL in the worker.
153+ url : url ? fileURLToPath ( url ) : undefined ,
154+ } ,
155+ } ) ;
114156 } ) ;
115157 }
116158
@@ -147,37 +189,19 @@ export class SassWorkerImplementation {
147189 this . availableWorkers . push ( request . workerIndex ) ;
148190
149191 if ( response . result ) {
150- // The results are expected to be Node.js `Buffer` objects but will each be transferred as
151- // a Uint8Array that does not have the expected `toString` behavior of a `Buffer`.
152- const { css, map, stats } = response . result ;
153- const result : CompileResult = {
154- // This `Buffer.from` override will use the memory directly and avoid making a copy
155- css : Buffer . from ( css . buffer , css . byteOffset , css . byteLength ) ,
156- stats,
157- } ;
158- if ( map ) {
159- // This `Buffer.from` override will use the memory directly and avoid making a copy
160- result . map = Buffer . from ( map . buffer , map . byteOffset , map . byteLength ) ;
161- }
162- request . callback ( undefined , result ) ;
192+ request . callback ( undefined , {
193+ ...response . result ,
194+ // URL is not serializable so in the worker we convert to string and here back to URL.
195+ loadedUrls : response . result . loadedUrls . map ( ( p ) => pathToFileURL ( p ) ) ,
196+ } ) ;
163197 } else {
164198 request . callback ( response . error ) ;
165199 }
166200 } ) ;
167201
168202 mainImporterPort . on (
169203 'message' ,
170- ( {
171- id,
172- url,
173- prev,
174- fromImport,
175- } : {
176- id : number ;
177- url : string ;
178- prev : string ;
179- fromImport : boolean ;
180- } ) => {
204+ ( { id, url, options } : { id : number ; url : string ; options : FileImporterOptions } ) => {
181205 const request = this . requests . get ( id ) ;
182206 if ( ! request ?. importers ) {
183207 mainImporterPort . postMessage ( null ) ;
@@ -187,7 +211,7 @@ export class SassWorkerImplementation {
187211 return ;
188212 }
189213
190- this . processImporters ( request . importers , url , prev , fromImport )
214+ this . processImporters ( request . importers , url , options )
191215 . then ( ( result ) => {
192216 mainImporterPort . postMessage ( result ) ;
193217 } )
@@ -207,44 +231,40 @@ export class SassWorkerImplementation {
207231 }
208232
209233 private async processImporters (
210- importers : Iterable < SyncImporter | AsyncImporter > ,
234+ importers : Iterable < Importers > ,
211235 url : string ,
212- prev : string ,
213- fromImport : boolean ,
214- ) : Promise < ImporterResult > {
215- let result = null ;
236+ options : FileImporterOptions ,
237+ ) : Promise < string | null > {
216238 for ( const importer of importers ) {
217- result = await new Promise < ImporterResult > ( ( resolve ) => {
218- // Importers can be both sync and async
219- const innerResult = ( importer as AsyncImporter ) . call (
220- { fromImport } as ImporterThis ,
221- url ,
222- prev ,
223- resolve ,
224- ) ;
225- if ( innerResult !== undefined ) {
226- resolve ( innerResult ) ;
227- }
228- } ) ;
239+ if ( this . isImporter ( importer ) ) {
240+ // Importer
241+ throw new Error ( 'Only File Importers are supported.' ) ;
242+ }
229243
244+ // File importer (Can be sync or aync).
245+ const result = await importer . findFileUrl ( url , options ) ;
230246 if ( result ) {
231- break ;
247+ return fileURLToPath ( result ) ;
232248 }
233249 }
234250
235- return result ;
251+ return null ;
236252 }
237253
238254 private createRequest (
239255 workerIndex : number ,
240256 callback : RenderCallback ,
241- importer : SyncImporter | AsyncImporter | ( SyncImporter | AsyncImporter ) [ ] | undefined ,
257+ importers : Importers [ ] | undefined ,
242258 ) : RenderRequest {
243259 return {
244260 id : this . idCounter ++ ,
245261 workerIndex,
246262 callback,
247- importers : ! importer || Array . isArray ( importer ) ? importer : [ importer ] ,
263+ importers,
248264 } ;
249265 }
266+
267+ private isImporter ( value : Importers ) : value is Importer {
268+ return 'canonicalize' in value && 'load' in value ;
269+ }
250270}
0 commit comments