7
7
*/
8
8
9
9
import {
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 ,
17
16
} from 'sass' ;
17
+ import { fileURLToPath , pathToFileURL } from 'url' ;
18
18
import { MessageChannel , Worker } from 'worker_threads' ;
19
19
import { maxWorkers } from '../utils/environment-options' ;
20
20
@@ -28,23 +28,34 @@ const MAX_RENDER_WORKERS = maxWorkers;
28
28
*/
29
29
type RenderCallback = ( error ?: Exception , result ?: CompileResult ) => void ;
30
30
31
+ type FileImporterOptions = Parameters < FileImporter [ 'findFileUrl' ] > [ 1 ] ;
32
+
31
33
/**
32
34
* An object containing the contextual information for a specific render request.
33
35
*/
34
36
interface RenderRequest {
35
37
id : number ;
36
38
workerIndex : number ;
37
39
callback : RenderCallback ;
38
- importers ?: ( SyncImporter | AsyncImporter ) [ ] ;
40
+ importers ?: Importers [ ] ;
39
41
}
40
42
43
+ /**
44
+ * All available importer types.
45
+ */
46
+ type Importers =
47
+ | Importer < 'sync' >
48
+ | Importer < 'async' >
49
+ | FileImporter < 'sync' >
50
+ | FileImporter < 'async' > ;
51
+
41
52
/**
42
53
* A response from the Sass render Worker containing the result of the operation.
43
54
*/
44
55
interface RenderResponseMessage {
45
56
id : number ;
46
57
error ?: Exception ;
47
- result ?: CompileResult ;
58
+ result ?: Omit < CompileResult , 'loadedUrls' > & { loadedUrls : string [ ] } ;
48
59
}
49
60
50
61
/**
@@ -71,46 +82,77 @@ export class SassWorkerImplementation {
71
82
/**
72
83
* The synchronous render function is not used by the `sass-loader`.
73
84
*/
74
- renderSync ( ) : never {
75
- throw new Error ( 'Sass renderSync is not supported.' ) ;
85
+ compileString ( ) : never {
86
+ throw new Error ( 'Sass compileString is not supported.' ) ;
76
87
}
77
88
78
89
/**
79
90
* Asynchronously request a Sass stylesheet to be renderered.
80
91
*
92
+ * @param source The contents to compile.
81
93
* @param options The `dart-sass` options to use when rendering the stylesheet.
82
- * @param callback The function to execute when the rendering is complete.
83
94
*/
84
- render ( options : Options < 'async' > , callback : RenderCallback ) : void {
95
+ compileStringAsync (
96
+ source : string ,
97
+ options : StringOptionsWithImporter < 'async' > | StringOptionsWithoutImporter < 'async' > ,
98
+ ) : Promise < CompileResult > {
85
99
// The `functions`, `logger` and `importer` options are JavaScript functions that cannot be transferred.
86
100
// 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 ;
88
102
89
103
// The CLI's configuration does not use or expose the ability to defined custom Sass functions
90
104
if ( functions && Object . keys ( functions ) . length > 0 ) {
91
105
throw new Error ( 'Sass custom functions are not supported.' ) ;
92
106
}
93
107
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
+ }
103
119
}
104
120
}
105
- }
106
121
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
+ }
109
128
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
+ } ) ;
114
156
} ) ;
115
157
}
116
158
@@ -147,37 +189,19 @@ export class SassWorkerImplementation {
147
189
this . availableWorkers . push ( request . workerIndex ) ;
148
190
149
191
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
+ } ) ;
163
197
} else {
164
198
request . callback ( response . error ) ;
165
199
}
166
200
} ) ;
167
201
168
202
mainImporterPort . on (
169
203
'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 } ) => {
181
205
const request = this . requests . get ( id ) ;
182
206
if ( ! request ?. importers ) {
183
207
mainImporterPort . postMessage ( null ) ;
@@ -187,7 +211,7 @@ export class SassWorkerImplementation {
187
211
return ;
188
212
}
189
213
190
- this . processImporters ( request . importers , url , prev , fromImport )
214
+ this . processImporters ( request . importers , url , options )
191
215
. then ( ( result ) => {
192
216
mainImporterPort . postMessage ( result ) ;
193
217
} )
@@ -207,44 +231,40 @@ export class SassWorkerImplementation {
207
231
}
208
232
209
233
private async processImporters (
210
- importers : Iterable < SyncImporter | AsyncImporter > ,
234
+ importers : Iterable < Importers > ,
211
235
url : string ,
212
- prev : string ,
213
- fromImport : boolean ,
214
- ) : Promise < ImporterResult > {
215
- let result = null ;
236
+ options : FileImporterOptions ,
237
+ ) : Promise < string | null > {
216
238
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
+ }
229
243
244
+ // File importer (Can be sync or aync).
245
+ const result = await importer . findFileUrl ( url , options ) ;
230
246
if ( result ) {
231
- break ;
247
+ return fileURLToPath ( result ) ;
232
248
}
233
249
}
234
250
235
- return result ;
251
+ return null ;
236
252
}
237
253
238
254
private createRequest (
239
255
workerIndex : number ,
240
256
callback : RenderCallback ,
241
- importer : SyncImporter | AsyncImporter | ( SyncImporter | AsyncImporter ) [ ] | undefined ,
257
+ importers : Importers [ ] | undefined ,
242
258
) : RenderRequest {
243
259
return {
244
260
id : this . idCounter ++ ,
245
261
workerIndex,
246
262
callback,
247
- importers : ! importer || Array . isArray ( importer ) ? importer : [ importer ] ,
263
+ importers,
248
264
} ;
249
265
}
266
+
267
+ private isImporter ( value : Importers ) : value is Importer {
268
+ return 'canonicalize' in value && 'load' in value ;
269
+ }
250
270
}
0 commit comments