Skip to content

Commit 90a9170

Browse files
feat: add api "auto" and use it by default (#1319)
1 parent 07c921b commit 90a9170

9 files changed

Lines changed: 98 additions & 18 deletions

.changeset/auto-api-default.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"sass-loader": major
3+
---
4+
5+
Add `"auto"` to the `api` option and make it the default. When the Sass implementation supports the modern compiler, `"auto"` enables it and reuses a single long-running compiler across files, which significantly improves build performance; otherwise it falls back to the `modern` API.

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -657,16 +657,18 @@ module.exports = {
657657
Type:
658658

659659
```ts
660-
type api = "modern" | "modern-compiler";
660+
type api = "auto" | "modern" | "modern-compiler";
661661
```
662662

663-
Default: `"modern"` for `sass` (`dart-sass`) and `sass-embedded`
663+
Default: `"auto"` for `sass` (`dart-sass`) and `sass-embedded`
664664

665665
Allows you to switch between the `modern` and `modern-compiler` APIs. You can find more information [here](https://sass-lang.com/documentation/js-api). The `modern-compiler` option enables the modern API with support for [Shared Resources](https://github.com/sass/sass/blob/main/accepted/shared-resources.d.ts.md).
666666

667+
When `"auto"` is used, the loader picks `"modern-compiler"` whenever the implementation exposes `initAsyncCompiler` (i.e. recent versions of `sass` and `sass-embedded`) and falls back to `"modern"` otherwise. Combined with `sass-embedded`, this yields the best build performance out of the box.
668+
667669
> [!NOTE]
668670
>
669-
> Using `modern-compiler` and `sass-embedded` together significantly improves performance and decreases build time. We strongly recommend their use. We will enable them by default in a future major release.
671+
> Using `modern-compiler` and `sass-embedded` together significantly improves performance and decreases build time. They are now selected automatically by the default `"auto"` API.
670672
671673
> [!NOTE]
672674
>

src/index.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ async function loader(content) {
3535

3636
const useSourceMap =
3737
typeof options.sourceMap === "boolean" ? options.sourceMap : this.sourceMap;
38-
const apiType = typeof options.api === "undefined" ? "modern" : options.api;
3938
const sassOptions = await getSassOptions(
4039
this,
4140
options,
@@ -59,7 +58,7 @@ async function loader(content) {
5958
let compile;
6059

6160
try {
62-
compile = getCompileFn(this, implementation, apiType);
61+
compile = getCompileFn(this, implementation, options.api);
6362
} catch (error) {
6463
callback(error);
6564
return;

src/options.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
]
1616
},
1717
"api": {
18-
"description": "Switch between `modern` and `modern-compiler` API for `sass` (`Dart Sass`) and `Sass Embedded` implementations.",
18+
"description": "Switch between `auto`, `modern` and `modern-compiler` API for `sass` (`Dart Sass`) and `Sass Embedded` implementations.",
1919
"link": "https://github.com/webpack/sass-loader#sassoptions",
20-
"enum": ["modern", "modern-compiler"]
20+
"enum": ["auto", "modern", "modern-compiler"]
2121
},
2222
"sassOptions": {
2323
"description": "Options for `sass` (`Dart Sass`) or `sass-embedded` implementation.",

src/utils.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -585,11 +585,15 @@ const sassModernCompilers = new WeakMap();
585585
* Verifies that the implementation and version of Sass is supported by this loader.
586586
* @param {LoaderContext} loaderContext loader context
587587
* @param {SassImplementation} implementation sass implementation
588-
* @param {"modern" | "modern-compiler"} apiType api type
588+
* @param {"auto" | "modern" | "modern-compiler" | undefined} apiType api type
589589
* @returns {SassCompileFunction} compile function
590590
*/
591-
function getCompileFn(loaderContext, implementation, apiType) {
592-
if (apiType === "modern-compiler") {
591+
function getCompileFn(loaderContext, implementation, apiType = "auto") {
592+
if (
593+
apiType === "modern-compiler" ||
594+
(apiType === "auto" &&
595+
typeof implementation.initAsyncCompiler === "function")
596+
) {
593597
return async (sassOptions) => {
594598
const webpackCompiler = loaderContext._compiler;
595599
const { data, ...rest } = sassOptions;

test/__snapshots__/implementation-option.test.js.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ exports[`implementation option 'sass-embedded', 'modern-compiler' API: errors 1`
2020

2121
exports[`implementation option 'sass-embedded', 'modern-compiler' API: warnings 1`] = `[]`;
2222

23+
exports[`implementation option auto API falls back to modern when initAsyncCompiler is absent: errors 1`] = `[]`;
24+
25+
exports[`implementation option auto API falls back to modern when initAsyncCompiler is absent: warnings 1`] = `[]`;
26+
27+
exports[`implementation option not specify with auto API: errors 1`] = `[]`;
28+
29+
exports[`implementation option not specify with auto API: warnings 1`] = `[]`;
30+
2331
exports[`implementation option not specify with modern API: errors 1`] = `[]`;
2432

2533
exports[`implementation option not specify with modern API: warnings 1`] = `[]`;

test/__snapshots__/validate-options.test.js.snap

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,24 @@ exports[`validate options should throw an error on the "additionalData" option w
1414
exports[`validate options should throw an error on the "api" option with "legacy" value 1`] = `
1515
"Invalid options object. Sass Loader has been initialized using an options object that does not match the API schema.
1616
- options.api should be one of these:
17-
"modern" | "modern-compiler"
18-
-> Switch between \`modern\` and \`modern-compiler\` API for \`sass\` (\`Dart Sass\`) and \`Sass Embedded\` implementations.
17+
"auto" | "modern" | "modern-compiler"
18+
-> Switch between \`auto\`, \`modern\` and \`modern-compiler\` API for \`sass\` (\`Dart Sass\`) and \`Sass Embedded\` implementations.
1919
-> Read more at https://github.com/webpack/sass-loader#sassoptions"
2020
`;
2121

2222
exports[`validate options should throw an error on the "api" option with "string" value 1`] = `
2323
"Invalid options object. Sass Loader has been initialized using an options object that does not match the API schema.
2424
- options.api should be one of these:
25-
"modern" | "modern-compiler"
26-
-> Switch between \`modern\` and \`modern-compiler\` API for \`sass\` (\`Dart Sass\`) and \`Sass Embedded\` implementations.
25+
"auto" | "modern" | "modern-compiler"
26+
-> Switch between \`auto\`, \`modern\` and \`modern-compiler\` API for \`sass\` (\`Dart Sass\`) and \`Sass Embedded\` implementations.
2727
-> Read more at https://github.com/webpack/sass-loader#sassoptions"
2828
`;
2929

3030
exports[`validate options should throw an error on the "api" option with "true" value 1`] = `
3131
"Invalid options object. Sass Loader has been initialized using an options object that does not match the API schema.
3232
- options.api should be one of these:
33-
"modern" | "modern-compiler"
34-
-> Switch between \`modern\` and \`modern-compiler\` API for \`sass\` (\`Dart Sass\`) and \`Sass Embedded\` implementations.
33+
"auto" | "modern" | "modern-compiler"
34+
-> Switch between \`auto\`, \`modern\` and \`modern-compiler\` API for \`sass\` (\`Dart Sass\`) and \`Sass Embedded\` implementations.
3535
-> Read more at https://github.com/webpack/sass-loader#sassoptions"
3636
`;
3737

test/implementation-option.test.js

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,77 @@ describe("implementation option", () => {
149149
expect(getWarnings(stats)).toMatchSnapshot("warnings");
150150
expect(getErrors(stats)).toMatchSnapshot("errors");
151151

152-
expect(sassEmbeddedSpyModernAPI).toHaveBeenCalledTimes(1);
152+
// Default is `auto`, which prefers `modern-compiler` when the
153+
// implementation exposes `initAsyncCompiler` (sass-embedded does).
154+
expect(sassEmbeddedCompilerSpies.compileStringSpy).toHaveBeenCalledTimes(1);
155+
expect(sassEmbeddedSpyModernAPI).not.toHaveBeenCalled();
156+
expect(dartSassSpyModernAPI).not.toHaveBeenCalled();
157+
expect(dartSassCompilerSpies.compileStringSpy).not.toHaveBeenCalled();
158+
159+
sassEmbeddedCompilerSpies.mockClear();
160+
sassEmbeddedSpyModernAPI.mockClear();
161+
dartSassSpyModernAPI.mockClear();
162+
163+
await close(compiler);
164+
});
165+
166+
it("not specify with auto API", async () => {
167+
const testId = getTestId("language", "scss");
168+
const options = {
169+
api: "auto",
170+
};
171+
const compiler = getCompiler(testId, { loader: { options } });
172+
const stats = await compile(compiler);
173+
const { css, sourceMap } = getCodeFromBundle(stats, compiler);
174+
175+
expect(css).toBeDefined();
176+
expect(sourceMap).toBeUndefined();
177+
178+
expect(getWarnings(stats)).toMatchSnapshot("warnings");
179+
expect(getErrors(stats)).toMatchSnapshot("errors");
180+
181+
expect(sassEmbeddedCompilerSpies.compileStringSpy).toHaveBeenCalledTimes(1);
182+
expect(sassEmbeddedSpyModernAPI).not.toHaveBeenCalled();
153183
expect(dartSassSpyModernAPI).not.toHaveBeenCalled();
184+
expect(dartSassCompilerSpies.compileStringSpy).not.toHaveBeenCalled();
154185

186+
sassEmbeddedCompilerSpies.mockClear();
155187
sassEmbeddedSpyModernAPI.mockClear();
156188
dartSassSpyModernAPI.mockClear();
157189

158190
await close(compiler);
159191
});
160192

193+
it("auto API falls back to modern when initAsyncCompiler is absent", async () => {
194+
const testId = getTestId("language", "scss");
195+
const fakeImplementation = {
196+
...dartSass,
197+
initAsyncCompiler: undefined,
198+
};
199+
const options = {
200+
api: "auto",
201+
implementation: fakeImplementation,
202+
};
203+
const compiler = getCompiler(testId, { loader: { options } });
204+
const stats = await compile(compiler);
205+
const { css, sourceMap } = getCodeFromBundle(stats, compiler);
206+
207+
expect(css).toBeDefined();
208+
expect(sourceMap).toBeUndefined();
209+
210+
expect(getWarnings(stats)).toMatchSnapshot("warnings");
211+
expect(getErrors(stats)).toMatchSnapshot("errors");
212+
213+
expect(dartSassSpyModernAPI).toHaveBeenCalledTimes(1);
214+
expect(dartSassCompilerSpies.compileStringSpy).not.toHaveBeenCalled();
215+
expect(sassEmbeddedSpyModernAPI).not.toHaveBeenCalled();
216+
expect(sassEmbeddedCompilerSpies.compileStringSpy).not.toHaveBeenCalled();
217+
218+
dartSassSpyModernAPI.mockClear();
219+
220+
await close(compiler);
221+
});
222+
161223
it("not specify with modern API", async () => {
162224
const testId = getTestId("language", "scss");
163225
const options = {

test/validate-options.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe("validate options", () => {
3737
failure: ["string"],
3838
},
3939
api: {
40-
success: ["modern", "modern-compiler"],
40+
success: ["auto", "modern", "modern-compiler"],
4141
failure: ["legacy", "string", true],
4242
},
4343
unknown: {

0 commit comments

Comments
 (0)