Skip to content

Commit 740b223

Browse files
committed
feat(@angular-devkit/build-angular): switch to use Sass modern API
Sass modern API provides faster compilations times when used in an async manner. Application compilation duration | Sass API and Compiler -- | -- 60852ms | dart-sass legacy sync API 52666ms | dart-sass modern API Note: https://github.com/johannesjo/super-productivity was used for benchmarking. Prior art: http://docs/document/d/1CvEceWMpBoEBd8SfvksGMdVHxaZMH93b0EGS3XbR3_Q?resourcekey=0-vFm-xMspT65FZLIyX7xWFQ BREAKING CHANGE: Deprecated support for tilde import has been removed. Please update the imports by removing the `~`. Before ```scss @import "~font-awesome/scss/font-awesome"; ``` After ```scss @import "font-awesome/scss/font-awesome"; ```
1 parent 88c3b71 commit 740b223

File tree

4 files changed

+213
-153
lines changed

4 files changed

+213
-153
lines changed

packages/angular_devkit/build_angular/src/builders/browser/specs/styles_spec.ts

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -260,19 +260,14 @@ describe('Browser Builder styles', () => {
260260
});
261261
});
262262

263-
/**
264-
* font-awesome mock to avoid having an extra dependency.
265-
*/
266-
function mockFontAwesomePackage(host: TestProjectHost): void {
263+
it(`supports font-awesome imports`, async () => {
264+
// font-awesome mock to avoid having an extra dependency.
267265
host.writeMultipleFiles({
268266
'node_modules/font-awesome/scss/font-awesome.scss': `
269-
* { color: red }
267+
* { color: red }
270268
`,
271269
});
272-
}
273270

274-
it(`supports font-awesome imports`, async () => {
275-
mockFontAwesomePackage(host);
276271
host.writeMultipleFiles({
277272
'src/styles.scss': `
278273
@import "font-awesome/scss/font-awesome";
@@ -283,19 +278,6 @@ describe('Browser Builder styles', () => {
283278
await browserBuild(architect, host, target, overrides);
284279
});
285280

286-
it(`supports font-awesome imports (tilde)`, async () => {
287-
mockFontAwesomePackage(host);
288-
host.writeMultipleFiles({
289-
'src/styles.scss': `
290-
$fa-font-path: "~font-awesome/fonts";
291-
@import "~font-awesome/scss/font-awesome";
292-
`,
293-
});
294-
295-
const overrides = { styles: [`src/styles.scss`] };
296-
await browserBuild(architect, host, target, overrides);
297-
});
298-
299281
it(`uses autoprefixer`, async () => {
300282
host.writeMultipleFiles({
301283
'src/styles.css': tags.stripIndents`

packages/angular_devkit/build_angular/src/sass/sass-service.ts

Lines changed: 96 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
*/
88

99
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,
1716
} from 'sass';
17+
import { fileURLToPath, pathToFileURL } from 'url';
1818
import { MessageChannel, Worker } from 'worker_threads';
1919
import { maxWorkers } from '../utils/environment-options';
2020

@@ -28,23 +28,34 @@ const MAX_RENDER_WORKERS = maxWorkers;
2828
*/
2929
type 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
*/
3436
interface 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
*/
4455
interface 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

Comments
 (0)