Skip to content

Commit

Permalink
feat!: use modern Sass JS API by default for sass and `sass-embedde…
Browse files Browse the repository at this point in the history
…d` (#1220)
  • Loading branch information
alexander-akait committed Jul 26, 2024
1 parent 57eee6b commit c7ca4ee
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 22 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ Type:
type api = "legacy" | "modern" | "modern-compiler";
```

Default: `"legacy"`
Default: `"modern"` for `sass` (`dart-sass`) and `sass-embedded` or `"legacy"` for `node-sass`

Allows you to switch between the `legacy` and `modern` 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).

Expand All @@ -709,7 +709,7 @@ module.exports = {
{
loader: "sass-loader",
options: {
api: "modern",
api: "modern-compiler",
sassOptions: {
// Your sass options
},
Expand Down
14 changes: 11 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,29 @@ async function loader(content) {

const useSourceMap =
typeof options.sourceMap === "boolean" ? options.sourceMap : this.sourceMap;
// Use `legacy` for `node-sass` and `modern` for `dart-sass` and `sass-embedded`
const apiType =
typeof implementation.compileStringAsync === "undefined"
? "legacy"
: typeof options.api === "undefined"
? "modern"
: options.api;
const sassOptions = await getSassOptions(
this,
options,
content,
implementation,
useSourceMap,
apiType,
);

const shouldUseWebpackImporter =
typeof options.webpackImporter === "boolean"
? options.webpackImporter
: true;

if (shouldUseWebpackImporter) {
const isModernAPI =
options.api === "modern" || options.api === "modern-compiler";
const isModernAPI = apiType === "modern" || apiType === "modern-compiler";

if (!isModernAPI) {
const { includePaths } = sassOptions;
Expand All @@ -67,7 +75,7 @@ async function loader(content) {
let compile;

try {
compile = getCompileFn(this, implementation, options);
compile = getCompileFn(this, implementation, apiType);
} catch (error) {
callback(error);
return;
Expand Down
25 changes: 10 additions & 15 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ function proxyCustomImporters(importers, loaderContext) {
* @param {string} content
* @param {object} implementation
* @param {boolean} useSourceMap
* @param {"legacy" | "modern" | "modern-compiler"} apiType
* @returns {Object}
*/
async function getSassOptions(
Expand All @@ -102,6 +103,7 @@ async function getSassOptions(
content,
implementation,
useSourceMap,
apiType,
) {
const options = loaderOptions.sassOptions
? typeof loaderOptions.sassOptions === "function"
Expand Down Expand Up @@ -174,8 +176,7 @@ async function getSassOptions(
};
}

const isModernAPI =
loaderOptions.api === "modern" || loaderOptions.api === "modern-compiler";
const isModernAPI = apiType === "modern" || apiType === "modern-compiler";
const { resourcePath } = loaderContext;

if (isModernAPI) {
Expand Down Expand Up @@ -477,9 +478,7 @@ function getWebpackResolver(
includePaths = [],
) {
const isModernSass =
implementation &&
(implementation.info.includes("dart-sass") ||
implementation.info.includes("sass-embedded"));
implementation && typeof implementation.compileStringAsync !== "undefined";
// We only have one difference with the built-in sass resolution logic and out resolution logic:
// First, we look at the files starting with `_`, then without `_` (i.e. `_name.sass`, `_name.scss`, `_name.css`, `name.sass`, `name.scss`, `name.css`),
// although `sass` look together by extensions (i.e. `_name.sass`/`name.sass`/`_name.scss`/`name.scss`/`_name.css`/`name.css`).
Expand Down Expand Up @@ -735,24 +734,20 @@ const sassModernCompilers = new WeakMap();
*
* @param {Object} loaderContext
* @param {Object} implementation
* @param {Object} options
* @param {"legacy" | "modern" | "modern-compiler"} apiType
* @returns {Function}
*/
function getCompileFn(loaderContext, implementation, options) {
const isNewSass =
implementation.info.includes("dart-sass") ||
implementation.info.includes("sass-embedded");

if (isNewSass) {
if (options.api === "modern") {
function getCompileFn(loaderContext, implementation, apiType) {
if (typeof implementation.compileStringAsync !== "undefined") {
if (apiType === "modern") {
return (sassOptions) => {
const { data, ...rest } = sassOptions;

return implementation.compileStringAsync(data, rest);
};
}

if (options.api === "modern-compiler") {
if (apiType === "modern-compiler") {
return async (sassOptions) => {
// eslint-disable-next-line no-underscore-dangle
const webpackCompiler = loaderContext._compiler;
Expand Down Expand Up @@ -799,7 +794,7 @@ function getCompileFn(loaderContext, implementation, options) {
});
}

if (options.api === "modern" || options.api === "modern-compiler") {
if (apiType === "modern" || apiType === "modern-compiler") {
throw new Error("Modern API is not supported for 'node-sass'");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ exports[`implementation option not specify with modern-compiler API: errors 1`]

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

exports[`implementation option not specify with node-sass: errors 1`] = `[]`;

exports[`implementation option not specify with node-sass: warnings 1`] = `[]`;

exports[`implementation option not specify: errors 1`] = `[]`;

exports[`implementation option not specify: warnings 1`] = `[]`;
Expand Down
4 changes: 4 additions & 0 deletions test/__snapshots__/implementation-option.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ exports[`implementation option not specify with modern-compiler API: errors 1`]

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

exports[`implementation option not specify with node-sass: errors 1`] = `[]`;

exports[`implementation option not specify with node-sass: warnings 1`] = `[]`;

exports[`implementation option not specify: errors 1`] = `[]`;

exports[`implementation option not specify: warnings 1`] = `[]`;
Expand Down
46 changes: 44 additions & 2 deletions test/implementation-option.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,49 @@ describe("implementation option", () => {
expect(getWarnings(stats)).toMatchSnapshot("warnings");
expect(getErrors(stats)).toMatchSnapshot("errors");

expect(sassEmbeddedSpy).toHaveBeenCalledTimes(1);
expect(sassEmbeddedSpy).toHaveBeenCalledTimes(0);
expect(sassEmbeddedSpyModernAPI).toHaveBeenCalledTimes(1);
expect(nodeSassSpy).toHaveBeenCalledTimes(0);
expect(dartSassSpy).toHaveBeenCalledTimes(0);
expect(dartSassSpyModernAPI).toHaveBeenCalledTimes(0);

sassEmbeddedSpy.mockClear();
sassEmbeddedSpyModernAPI.mockClear();
nodeSassSpy.mockClear();
dartSassSpy.mockClear();
dartSassSpyModernAPI.mockClear();

await close(compiler);
});

it("not specify with node-sass", async () => {
const testId = getTestId("language", "scss");
const options = {
implementation: nodeSass,
};
const compiler = getCompiler(testId, { loader: { options } });
const stats = await compile(compiler);
const { css, sourceMap } = getCodeFromBundle(stats, compiler);

expect(css).toBeDefined();
expect(sourceMap).toBeUndefined();

expect(getWarnings(stats)).toMatchSnapshot("warnings");
expect(getErrors(stats)).toMatchSnapshot("errors");

expect(sassEmbeddedSpy).toHaveBeenCalledTimes(0);
expect(sassEmbeddedSpyModernAPI).toHaveBeenCalledTimes(0);
expect(nodeSassSpy).toHaveBeenCalledTimes(isNodeSassSupported() ? 1 : 0);
expect(dartSassSpy).toHaveBeenCalledTimes(0);
expect(dartSassSpyModernAPI).toHaveBeenCalledTimes(
isNodeSassSupported() ? 0 : 1,
);

sassEmbeddedSpy.mockClear();
sassEmbeddedSpyModernAPI.mockClear();
nodeSassSpy.mockClear();
dartSassSpy.mockClear();
dartSassSpyModernAPI.mockClear();

await close(compiler);
});
Expand All @@ -216,8 +252,10 @@ describe("implementation option", () => {
expect(dartSassSpy).toHaveBeenCalledTimes(0);

sassEmbeddedSpy.mockClear();
sassEmbeddedSpyModernAPI.mockClear();
nodeSassSpy.mockClear();
dartSassSpy.mockClear();
dartSassSpyModernAPI.mockClear();

await close(compiler);
});
Expand All @@ -241,8 +279,10 @@ describe("implementation option", () => {
expect(nodeSassSpy).toHaveBeenCalledTimes(0);
expect(dartSassSpyModernAPI).toHaveBeenCalledTimes(0);

sassEmbeddedSpy.mockClear();
sassEmbeddedSpyModernAPI.mockClear();
nodeSassSpy.mockClear();
dartSassSpy.mockClear();
dartSassSpyModernAPI.mockClear();

await close(compiler);
Expand All @@ -269,9 +309,11 @@ describe("implementation option", () => {
expect(dartSassSpyModernAPI).toHaveBeenCalledTimes(0);
expect(dartSassCompilerSpies.compileStringSpy).toHaveBeenCalledTimes(0);

sassEmbeddedSpy.mockClear();
sassEmbeddedSpyModernAPI.mockClear();
nodeSassSpy.mockClear();
dartSassSpy.mockClear();
dartSassSpyModernAPI.mockClear();
dartSassCompilerSpies.mockClear();

await close(compiler);
});
Expand Down

0 comments on commit c7ca4ee

Please sign in to comment.