diff --git a/README.md b/README.md
index b6b42ff5..32771086 100644
--- a/README.md
+++ b/README.md
@@ -27,8 +27,14 @@ Looking for the webpack 1 loader? Check out the [archive/webpack-1 branch](https
npm install sass-loader node-sass webpack --save-dev
```
-The sass-loader requires [node-sass](https://github.com/sass/node-sass) and [webpack](https://github.com/webpack)
-as [`peerDependency`](https://docs.npmjs.com/files/package.json#peerdependencies). Thus you are able to control the versions accurately.
+The sass-loader requires [webpack](https://github.com/webpack) as a
+[`peerDependency`](https://docs.npmjs.com/files/package.json#peerdependencies)
+and it requires you to install either [Node Sass][] or [Dart Sass][] on your
+own. This allows you to control the versions of all your dependencies, and to
+choose which Sass implementation to use.
+
+[Node Sass]: https://github.com/sass/node-sass
+[Dart Sass]: http://sass-lang.com/dart-sass
Examples
@@ -48,14 +54,14 @@ module.exports = {
use: [
"style-loader", // creates style nodes from JS strings
"css-loader", // translates CSS into CommonJS
- "sass-loader" // compiles Sass to CSS
+ "sass-loader" // compiles Sass to CSS, using Node Sass by default
]
}]
}
};
```
-You can also pass options directly to [node-sass](https://github.com/andrew/node-sass) by specifying an `options` property like this:
+You can also pass options directly to [Node Sass][] or [Dart Sass][]:
```js
// webpack.config.js
@@ -79,7 +85,54 @@ module.exports = {
};
```
-See [node-sass](https://github.com/andrew/node-sass) for all available Sass options.
+See [the Node Sass documentation](https://github.com/sass/node-sass/blob/master/README.md#options) for all available Sass options.
+
+The special `implementation` option determines which implementation of Sass to
+use. It takes either a [Node Sass][] or a [Dart Sass][] module. For example, to
+use Dart Sass, you'd pass:
+
+```js
+// ...
+ {
+ loader: "sass-loader",
+ options: {
+ implementation: require("sass")
+ }
+ }
+// ...
+```
+
+Note that when using Dart Sass, **synchronous compilation is twice as fast as
+asynchronous compilation** by default, due to the overhead of asynchronous
+callbacks. To avoid this overhead, you can use the
+[`fibers`](https://www.npmjs.com/package/fibers) package to call asynchronous
+importers from the synchronous code path. To enable this, pass the `Fiber` class
+to the `fiber` option:
+
+```js
+// webpack.config.js
+const Fiber = require('fibers');
+
+module.exports = {
+ ...
+ module: {
+ rules: [{
+ test: /\.scss$/,
+ use: [{
+ loader: "style-loader"
+ }, {
+ loader: "css-loader"
+ }, {
+ loader: "sass-loader",
+ options: {
+ implementation: require("sass"),
+ fiber: Fiber
+ }
+ }]
+ }]
+ }
+};
+```
### In production
@@ -116,7 +169,7 @@ module.exports = {
### Imports
-webpack provides an [advanced mechanism to resolve files](https://webpack.js.org/concepts/module-resolution/). The sass-loader uses node-sass' custom importer feature to pass all queries to the webpack resolving engine. Thus you can import your Sass modules from `node_modules`. Just prepend them with a `~` to tell webpack that this is not a relative import:
+webpack provides an [advanced mechanism to resolve files](https://webpack.js.org/concepts/module-resolution/). The sass-loader uses Sass's custom importer feature to pass all queries to the webpack resolving engine. Thus you can import your Sass modules from `node_modules`. Just prepend them with a `~` to tell webpack that this is not a relative import:
```css
@import "~bootstrap/dist/css/bootstrap";
diff --git a/lib/loader.js b/lib/loader.js
index 531196f3..d3f30ec2 100644
--- a/lib/loader.js
+++ b/lib/loader.js
@@ -6,12 +6,9 @@ const formatSassError = require("./formatSassError");
const webpackImporter = require("./webpackImporter");
const normalizeOptions = require("./normalizeOptions");
const pify = require("pify");
+const semver = require("semver");
-// This queue makes sure node-sass leaves one thread available for executing
-// fs tasks when running the custom importer code.
-// This can be removed as soon as node-sass implements a fix for this.
-const threadPoolSize = process.env.UV_THREADPOOL_SIZE || 4;
-let asyncSassJobQueue = null;
+let nodeSassJobQueue = null;
/**
* The sass-loader makes node-sass available to webpack modules.
@@ -20,19 +17,6 @@ let asyncSassJobQueue = null;
* @param {string} content
*/
function sassLoader(content) {
- if (asyncSassJobQueue === null) {
- const sass = require("node-sass");
- const sassVersion = /^(\d+)/.exec(require("node-sass/package.json").version).pop();
-
- if (Number(sassVersion) < 4) {
- throw new Error(
- "The installed version of `node-sass` is not compatible (expected: >= 4, actual: " + sassVersion + ")."
- );
- }
-
- asyncSassJobQueue = async.queue(sass.render, threadPoolSize - 1);
- }
-
const callback = this.async();
const isSync = typeof callback !== "function";
const self = this;
@@ -59,8 +43,9 @@ function sassLoader(content) {
return;
}
- // start the actual rendering
- asyncSassJobQueue.push(options, (err, result) => {
+ const render = getRenderFuncFromSassImpl(options.implementation || require("node-sass"));
+
+ render(options, (err, result) => {
if (err) {
formatSassError(err, this.resourcePath);
err.file && this.dependency(err.file);
@@ -92,4 +77,48 @@ function sassLoader(content) {
});
}
+/**
+ * Verifies that the implementation and version of Sass is supported by this loader.
+ *
+ * @param {Object} module
+ * @returns {Function}
+ */
+function getRenderFuncFromSassImpl(module) {
+ const info = module.info;
+ const components = info.split("\t");
+
+ if (components.length < 2) {
+ throw new Error("Unknown Sass implementation \"" + info + "\".");
+ }
+
+ const implementation = components[0];
+ const version = components[1];
+
+ if (!semver.valid(version)) {
+ throw new Error("Invalid Sass version \"" + version + "\".");
+ }
+
+ if (implementation === "dart-sass") {
+ if (!semver.satisfies(version, "^1.3.0")) {
+ throw new Error("Dart Sass version " + version + " is incompatible with ^1.3.0.");
+ }
+ return module.render.bind(module);
+ } else if (implementation === "node-sass") {
+ if (!semver.satisfies(version, "^4.0.0")) {
+ throw new Error("Node Sass version " + version + " is incompatible with ^4.0.0.");
+ }
+ // There is an issue with node-sass when async custom importers are used
+ // See https://github.com/sass/node-sass/issues/857#issuecomment-93594360
+ // We need to use a job queue to make sure that one thread is always available to the UV lib
+ if (nodeSassJobQueue === null) {
+ const threadPoolSize = Number(process.env.UV_THREADPOOL_SIZE || 4);
+
+ nodeSassJobQueue = async.queue(module.render.bind(module), threadPoolSize - 1);
+ }
+
+ return nodeSassJobQueue.push.bind(nodeSassJobQueue);
+ }
+ throw new Error("Unknown Sass implementation \"" + implementation + "\".");
+}
+
module.exports = sassLoader;
diff --git a/package-lock.json b/package-lock.json
index 2f89655f..d0b207f8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1271,7 +1271,7 @@
"json-stringify-safe": "5.0.1",
"lodash": "4.17.4",
"meow": "3.7.0",
- "semver": "5.3.0",
+ "semver": "5.5.0",
"split": "1.0.0",
"through2": "2.0.3"
}
@@ -3728,7 +3728,7 @@
"dev": true,
"requires": {
"meow": "3.7.0",
- "semver": "5.3.0"
+ "semver": "5.5.0"
}
},
"gitconfiglocal": {
@@ -5436,6 +5436,14 @@
"semver": "5.3.0",
"tar": "2.2.1",
"which": "1.2.14"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+ "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
+ "dev": true
+ }
}
},
"node-libs-browser": {
@@ -5512,7 +5520,7 @@
"requires": {
"hosted-git-info": "2.5.0",
"is-builtin-module": "1.0.0",
- "semver": "5.3.0",
+ "semver": "5.5.0",
"validate-npm-package-license": "3.0.1"
}
},
@@ -8888,6 +8896,12 @@
"ret": "0.1.15"
}
},
+ "sass": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.5.1.tgz",
+ "integrity": "sha512-C9s7SFttwy5OnXDs0ZFVv7c659A7GG/0R0VyrQaGXR07cucSJA2E5XN2ZhLF3kklN90aXuPD2rphkNWyeb6y2A==",
+ "dev": true
+ },
"sass-graph": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",
@@ -8966,10 +8980,9 @@
}
},
"semver": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
- "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
- "dev": true
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
+ "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
},
"send": {
"version": "0.15.3",
@@ -9508,7 +9521,7 @@
"conventional-recommended-bump": "1.0.0",
"figures": "1.7.0",
"fs-access": "1.0.1",
- "semver": "5.3.0",
+ "semver": "5.5.0",
"yargs": "8.0.2"
},
"dependencies": {
diff --git a/package.json b/package.json
index 67944a93..cd4c8f37 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,8 @@
"loader-utils": "^1.0.1",
"lodash.tail": "^4.1.1",
"neo-async": "^2.5.0",
- "pify": "^3.0.0"
+ "pify": "^3.0.0",
+ "semver": "^5.5.0"
},
"devDependencies": {
"bootstrap-sass": "^3.3.5",
@@ -44,6 +45,7 @@
"node-sass": "^4.5.0",
"nyc": "^11.0.2",
"raw-loader": "^0.5.1",
+ "sass": "^1.3.0",
"should": "^11.2.0",
"standard-version": "^4.2.0",
"style-loader": "^0.18.2",
diff --git a/test/index.test.js b/test/index.test.js
index 1826b70f..cb5130a0 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -6,6 +6,8 @@ const path = require("path");
const webpack = require("webpack");
const fs = require("fs");
const merge = require("webpack-merge");
+const nodeSass = require("node-sass");
+const dartSass = require("sass");
const customImporter = require("./tools/customImporter.js");
const customFunctions = require("./tools/customFunctions.js");
const pathToSassLoader = require.resolve("../lib/loader.js");
@@ -14,6 +16,7 @@ const sassLoader = require(pathToSassLoader);
const mockRequire = require("mock-require");
const CR = /\r/g;
+const implementations = [nodeSass, dartSass];
const syntaxStyles = ["scss", "sass"];
const pathToErrorFileNotFound = path.resolve(__dirname, "./scss/error-file-not-found.scss");
const pathToErrorFileNotFound2 = path.resolve(__dirname, "./scss/error-file-not-found-2.scss");
@@ -32,299 +35,381 @@ Object.defineProperty(loaderContextMock, "options", {
}
});
-syntaxStyles.forEach(ext => {
- function execTest(testId, loaderOptions, webpackOptions) {
- return new Promise((resolve, reject) => {
- const baseConfig = merge({
- entry: path.join(__dirname, ext, testId + "." + ext),
- output: {
- filename: "bundle." + ext + ".js"
- },
- module: {
- rules: [{
- test: new RegExp(`\\.${ ext }$`),
- use: [
- { loader: "raw-loader" },
- { loader: pathToSassLoader, options: loaderOptions }
- ]
- }]
- }
- }, webpackOptions);
+implementations.forEach(implementation => {
+ const implementationName = implementation.info.split("\t")[0];
- runWebpack(baseConfig, (err) => err ? reject(err) : resolve());
- }).then(() => {
- const actualCss = readBundle("bundle." + ext + ".js");
- const expectedCss = readCss(ext, testId);
+ describe(implementationName, () => {
+ syntaxStyles.forEach(ext => {
+ function execTest(testId, loaderOptions, webpackOptions) {
+ const bundleName = "bundle." + ext + "." + implementationName + ".js";
- // writing the actual css to output-dir for better debugging
- // fs.writeFileSync(path.join(__dirname, "output", `${ testId }.${ ext }.css`), actualCss, "utf8");
- actualCss.should.eql(expectedCss);
- });
- }
+ return new Promise((resolve, reject) => {
+ const baseConfig = merge({
+ entry: path.join(__dirname, ext, testId + "." + ext),
+ output: {
+ filename: bundleName
+ }
+ }, webpackOptions);
- describe(`sass-loader (${ ext })`, () => {
- describe("basic", () => {
- it("should compile simple sass without errors", () => execTest("language"));
- });
- describe("imports", () => {
- it("should resolve imports correctly", () => execTest("imports"));
- // Test for issue: https://github.com/webpack-contrib/sass-loader/issues/32
- it("should pass with multiple imports", () => execTest("multiple-imports"));
- // Test for issue: https://github.com/webpack-contrib/sass-loader/issues/73
- it("should resolve imports from other language style correctly", () => execTest("import-other-style"));
- // Test for includePath imports
- it("should resolve imports from another directory declared by includePaths correctly", () => execTest("import-include-paths", {
- includePaths: [path.join(__dirname, ext, "includePath")]
- }));
- it("should not resolve CSS imports", () => execTest("import-css"));
- it("should compile bootstrap-sass without errors", () => execTest("bootstrap-sass"));
- it("should correctly import scoped npm packages", () => execTest("import-from-npm-org-pkg"));
- it("should resolve aliases", () => execTest("import-alias", {}, {
- resolve: {
- alias: {
- "path-to-alias": path.join(__dirname, ext, "another", "alias." + ext)
+ runWebpack(baseConfig, loaderOptions, (err) => err ? reject(err) : resolve());
+ }).then(() => {
+ const actualCss = readBundle(bundleName);
+ const expectedCss = readCss(ext, testId);
+
+ // writing the actual css to output-dir for better debugging
+ // fs.writeFileSync(path.join(__dirname, "output", `${ testId }.${ ext }.css`), actualCss, "utf8");
+ actualCss.should.eql(expectedCss);
+ });
+ }
+
+ describe(`sass-loader (${ ext })`, () => {
+ describe("basic", () => {
+ it("should compile simple sass without errors", () => execTest("language"));
+ });
+ describe("imports", () => {
+ it("should resolve imports correctly", () => execTest("imports"));
+ // Test for issue: https://github.com/webpack-contrib/sass-loader/issues/32
+ it("should pass with multiple imports", () => execTest("multiple-imports"));
+ // Test for issue: https://github.com/webpack-contrib/sass-loader/issues/73
+ it("should resolve imports from other language style correctly", () => execTest("import-other-style"));
+ // Test for includePath imports
+ it("should resolve imports from another directory declared by includePaths correctly", () => execTest("import-include-paths", {
+ includePaths: [path.join(__dirname, ext, "includePath")]
+ }));
+ // Legacy support for CSS imports with node-sass
+ // See discussion https://github.com/webpack-contrib/sass-loader/pull/573/files?#r199109203
+ if (implementation === nodeSass) {
+ it("should not resolve CSS imports", () => execTest("import-css"));
}
- }
- }));
- });
- describe("custom importers", () => {
- it("should use custom importer", () => execTest("custom-importer", {
- importer: customImporter
- }));
- });
- describe("custom functions", () => {
- it("should expose custom functions", () => execTest("custom-functions", {
- functions: customFunctions
- }));
- });
- describe("prepending data", () => {
- it("should extend the data-option if present", () => execTest("prepending-data", {
- data: "$prepended-data: hotpink;"
- }));
- });
- // See https://github.com/webpack-contrib/sass-loader/issues/21
- describe("empty files", () => {
- it("should compile without errors", () => execTest("empty"));
+ it("should compile bootstrap-sass without errors", () => execTest("bootstrap-sass"));
+ it("should correctly import scoped npm packages", () => execTest("import-from-npm-org-pkg"));
+ it("should resolve aliases", () => execTest("import-alias", {}, {
+ resolve: {
+ alias: {
+ "path-to-alias": path.join(__dirname, ext, "another", "alias." + ext)
+ }
+ }
+ }));
+ });
+ describe("custom importers", () => {
+ it("should use custom importer", () => execTest("custom-importer", {
+ importer: customImporter
+ }));
+ });
+ describe("custom functions", () => {
+ it("should expose custom functions", () => execTest("custom-functions", {
+ functions: customFunctions(implementation)
+ }));
+ });
+ describe("prepending data", () => {
+ it("should extend the data-option if present", () => execTest("prepending-data", {
+ data: "$prepended-data: hotpink" + (ext === "sass" ? "\n" : ";")
+ }));
+ });
+ // See https://github.com/webpack-contrib/sass-loader/issues/21
+ describe("empty files", () => {
+ it("should compile without errors", () => execTest("empty"));
+ });
+ });
});
- });
-});
-describe("sass-loader", () => {
- describe("multiple compilations", () => {
- it("should not interfere with each other", () =>
- new Promise((resolve, reject) => {
- runWebpack({
- entry: {
- b: path.join(__dirname, "scss", "multipleCompilations", "b.scss"),
- c: path.join(__dirname, "scss", "multipleCompilations", "c.scss"),
- a: path.join(__dirname, "scss", "multipleCompilations", "a.scss"),
- d: path.join(__dirname, "scss", "multipleCompilations", "d.scss"),
- e: path.join(__dirname, "scss", "multipleCompilations", "e.scss"),
- f: path.join(__dirname, "scss", "multipleCompilations", "f.scss"),
- g: path.join(__dirname, "scss", "multipleCompilations", "g.scss"),
- h: path.join(__dirname, "scss", "multipleCompilations", "h.scss")
- },
- output: {
- filename: "bundle.multiple-compilations.[name].js"
- },
- module: {
- rules: [{
- test: /\.scss$/,
- use: [
- { loader: "raw-loader" },
- // We're specifying an empty options object because otherwise, webpack creates a new object for every loader invocation
- // Since we want to ensure that our loader is not tampering with the option object, we are triggering webpack to re-use the options object
- // @see https://github.com/webpack-contrib/sass-loader/issues/368#issuecomment-278330164
- { loader: pathToSassLoader, options: {} }
- ]
- }]
- }
- }, err => err ? reject(err) : resolve());
- })
- .then(() => {
- const expectedCss = readCss("scss", "imports");
- const a = readBundle("bundle.multiple-compilations.a.js");
- const b = readBundle("bundle.multiple-compilations.b.js");
- const c = readBundle("bundle.multiple-compilations.c.js");
- const d = readBundle("bundle.multiple-compilations.d.js");
- const e = readBundle("bundle.multiple-compilations.e.js");
- const f = readBundle("bundle.multiple-compilations.f.js");
- const g = readBundle("bundle.multiple-compilations.g.js");
- const h = readBundle("bundle.multiple-compilations.h.js");
+ describe("sass-loader", () => {
+ describe("multiple compilations", () => {
+ it("should not interfere with each other", () =>
+ new Promise((resolve, reject) => {
+ runWebpack({
+ entry: {
+ b: path.join(__dirname, "scss", "multipleCompilations", "b.scss"),
+ c: path.join(__dirname, "scss", "multipleCompilations", "c.scss"),
+ a: path.join(__dirname, "scss", "multipleCompilations", "a.scss"),
+ d: path.join(__dirname, "scss", "multipleCompilations", "d.scss"),
+ e: path.join(__dirname, "scss", "multipleCompilations", "e.scss"),
+ f: path.join(__dirname, "scss", "multipleCompilations", "f.scss"),
+ g: path.join(__dirname, "scss", "multipleCompilations", "g.scss"),
+ h: path.join(__dirname, "scss", "multipleCompilations", "h.scss")
+ },
+ output: {
+ filename: "bundle.multiple-compilations.[name].js"
+ }
+ }, {}, err => err ? reject(err) : resolve());
+ })
+ .then(() => {
+ const expectedCss = readCss("scss", "imports");
+ const a = readBundle("bundle.multiple-compilations.a.js");
+ const b = readBundle("bundle.multiple-compilations.b.js");
+ const c = readBundle("bundle.multiple-compilations.c.js");
+ const d = readBundle("bundle.multiple-compilations.d.js");
+ const e = readBundle("bundle.multiple-compilations.e.js");
+ const f = readBundle("bundle.multiple-compilations.f.js");
+ const g = readBundle("bundle.multiple-compilations.g.js");
+ const h = readBundle("bundle.multiple-compilations.h.js");
- a.should.equal(expectedCss);
- b.should.equal(expectedCss);
- c.should.equal(expectedCss);
- d.should.equal(expectedCss);
- e.should.equal(expectedCss);
- f.should.equal(expectedCss);
- g.should.equal(expectedCss);
- h.should.equal(expectedCss);
- })
- );
- });
- describe("source maps", () => {
- function buildWithSourceMaps() {
- return new Promise((resolve, reject) => {
- runWebpack({
- entry: path.join(__dirname, "scss", "imports.scss"),
- output: {
- filename: "bundle.source-maps.js"
- },
- devtool: "source-map",
- module: {
- rules: [{
- test: /\.scss$/,
- use: [
- { loader: testLoader.filename },
- {
- loader: pathToSassLoader, options: {
- sourceMap: true
- }
- }
- ]
- }]
- }
- }, err => err ? reject(err) : resolve());
+ a.should.equal(expectedCss);
+ b.should.equal(expectedCss);
+ c.should.equal(expectedCss);
+ d.should.equal(expectedCss);
+ e.should.equal(expectedCss);
+ f.should.equal(expectedCss);
+ g.should.equal(expectedCss);
+ h.should.equal(expectedCss);
+ })
+ );
});
- }
+ describe("source maps", () => {
+ function buildWithSourceMaps() {
+ return new Promise((resolve, reject) => {
+ webpack({
+ entry: path.join(__dirname, "scss", "imports.scss"),
+ mode: "development",
+ output: {
+ path: path.join(__dirname, "output"),
+ filename: "bundle.source-maps.js",
+ libraryTarget: "commonjs2"
+ },
+ devtool: "source-map",
+ module: {
+ rules: [{
+ test: /\.scss$/,
+ use: [
+ { loader: testLoader.filename },
+ {
+ loader: pathToSassLoader, options: {
+ implementation,
+ sourceMap: true
+ }
+ }
+ ]
+ }]
+ }
+ }, (webpackErr, stats) => {
+ const err = webpackErr ||
+ (stats.hasErrors() && stats.compilation.errors[0]) ||
+ (stats.hasWarnings() && stats.compilation.warnings[0]);
+
+ if (err) {
+ reject(err);
+ } else {
+ resolve();
+ }
+ });
+ });
+ }
- it("should compile without errors", () => buildWithSourceMaps());
- it("should produce a valid source map", () => {
- const cwdGetter = process.cwd;
- const fakeCwd = path.join(__dirname, "scss");
+ it("should compile without errors", () => buildWithSourceMaps());
+ it("should produce a valid source map", () => {
+ const cwdGetter = process.cwd;
+ const fakeCwd = path.join(__dirname, "scss");
- process.cwd = function () {
- return fakeCwd;
- };
+ process.cwd = function () {
+ return fakeCwd;
+ };
- return buildWithSourceMaps()
- .then(() => {
- const sourceMap = testLoader.sourceMap;
+ return buildWithSourceMaps()
+ .then(() => {
+ const sourceMap = testLoader.sourceMap;
- sourceMap.should.not.have.property("file");
- sourceMap.should.have.property("sourceRoot", fakeCwd);
- // This number needs to be updated if imports.scss or any dependency of that changes
- sourceMap.sources.should.have.length(12);
- sourceMap.sources.forEach(sourcePath =>
- fs.existsSync(path.resolve(sourceMap.sourceRoot, sourcePath))
- );
+ sourceMap.should.not.have.property("file");
+ sourceMap.should.have.property("sourceRoot", fakeCwd);
+ // This number needs to be updated if imports.scss or any dependency of that changes.
+ // Node Sass includes a duplicate entry, Dart Sass does not.
+ sourceMap.sources.should.have.length(implementation === nodeSass ? 11 : 10);
+ sourceMap.sources.forEach(sourcePath =>
+ fs.existsSync(path.resolve(sourceMap.sourceRoot, sourcePath))
+ );
- process.cwd = cwdGetter;
+ process.cwd = cwdGetter;
+ });
});
- });
- });
- describe("errors", () => {
- it("should throw an error in synchronous loader environments", () => {
- try {
- sassLoader.call(Object.create(loaderContextMock), "");
- } catch (err) {
- // check for file excerpt
- err.message.should.equal("Synchronous compilation is not supported anymore. See https://github.com/webpack-contrib/sass-loader/issues/333");
- }
- });
- it("should output understandable errors in entry files", (done) => {
- runWebpack({
- entry: pathToSassLoader + "!" + pathToErrorFile
- }, (err) => {
- err.message.should.match(/Property "some-value" must be followed by a ':'/);
- err.message.should.match(/\(line 2, column 5\)/);
- err.message.indexOf(pathToErrorFile).should.not.equal(-1);
- done();
});
- });
- it("should output understandable errors of imported files", (done) => {
- runWebpack({
- entry: pathToSassLoader + "!" + pathToErrorImport
- }, (err) => {
- // check for file excerpt
- err.message.should.match(/Property "some-value" must be followed by a ':'/);
- err.message.should.match(/\(line 2, column 5\)/);
- err.message.indexOf(pathToErrorFile).should.not.equal(-1);
- done();
- });
- });
- it("should output understandable errors when a file could not be found", (done) => {
- runWebpack({
- entry: pathToSassLoader + "!" + pathToErrorFileNotFound
- }, (err) => {
- err.message.should.match(/@import "does-not-exist";/);
- err.message.should.match(/File to import not found or unreadable: does-not-exist/);
- err.message.should.match(/\(line 1, column 1\)/);
- err.message.indexOf(pathToErrorFileNotFound).should.not.equal(-1);
- done();
- });
- });
- it("should not auto-resolve imports with explicit file names", (done) => {
- runWebpack({
- entry: pathToSassLoader + "!" + pathToErrorFileNotFound2
- }, (err) => {
- err.message.should.match(/@import "\.\/another\/_module\.scss";/);
- err.message.should.match(/File to import not found or unreadable: \.\/another\/_module\.scss/);
- err.message.should.match(/\(line 1, column 1\)/);
- err.message.indexOf(pathToErrorFileNotFound2).should.not.equal(-1);
- done();
- });
- });
- it("should not swallow errors when trying to load node-sass", (done) => {
- mockRequire.reRequire(pathToSassLoader);
- const module = require("module");
- const originalResolve = module._resolveFilename;
+ describe("errors", () => {
+ it("should throw an error in synchronous loader environments", () => {
+ try {
+ sassLoader.call(Object.create(loaderContextMock), "");
+ } catch (err) {
+ // check for file excerpt
+ err.message.should.equal("Synchronous compilation is not supported anymore. See https://github.com/webpack-contrib/sass-loader/issues/333");
+ }
+ });
+ it("should output understandable errors in entry files", (done) => {
+ runWebpack({
+ entry: pathToErrorFile
+ }, {}, (err) => {
+ if (implementation === nodeSass) {
+ err.message.should.match(/Property "some-value" must be followed by a ':'/);
+ err.message.should.match(/\(line 2, column 5\)/);
+ } else {
+ err.message.should.match(/Expected "{"./);
+ err.message.should.match(/\(line 3, column 1\)/);
+ }
+ err.message.indexOf(pathToErrorFile).should.not.equal(-1);
+ done();
+ });
+ });
+ it("should output understandable errors of imported files", (done) => {
+ runWebpack({
+ entry: pathToErrorImport
+ }, {}, (err) => {
+ // check for file excerpt
+ if (implementation === nodeSass) {
+ err.message.should.match(/Property "some-value" must be followed by a ':'/);
+ err.message.should.match(/\(line 2, column 5\)/);
+ } else {
+ err.message.should.match(/Expected "{"./);
+ err.message.should.match(/\(line 3, column 1\)/);
+ }
+ err.message.indexOf(pathToErrorFile).should.not.equal(-1);
+ done();
+ });
+ });
+ it("should output understandable errors when a file could not be found", (done) => {
+ runWebpack({
+ entry: pathToErrorFileNotFound
+ }, {}, (err) => {
+ err.message.should.match(/@import "does-not-exist";/);
+ if (implementation === nodeSass) {
+ err.message.should.match(/File to import not found or unreadable: does-not-exist/);
+ err.message.should.match(/\(line 1, column 1\)/);
+ } else {
+ err.message.should.match(/Can't find stylesheet to import\./);
+ err.message.should.match(/\(line 1, column 9\)/);
+ }
+ err.message.indexOf(pathToErrorFileNotFound).should.not.equal(-1);
+ done();
+ });
+ });
+ it("should not auto-resolve imports with explicit file names", (done) => {
+ runWebpack({
+ entry: pathToErrorFileNotFound2
+ }, {}, (err) => {
+ err.message.should.match(/@import "\.\/another\/_module\.scss";/);
+ if (implementation === nodeSass) {
+ err.message.should.match(/File to import not found or unreadable: \.\/another\/_module\.scss/);
+ err.message.should.match(/\(line 1, column 1\)/);
+ } else {
+ err.message.should.match(/Can't find stylesheet to import\./);
+ err.message.should.match(/\(line 1, column 9\)/);
+ }
+ err.message.indexOf(pathToErrorFileNotFound2).should.not.equal(-1);
+ done();
+ });
+ });
+ it("should not swallow errors when trying to load node-sass", (done) => {
+ mockRequire.reRequire(pathToSassLoader);
+ const module = require("module");
+ const originalResolve = module._resolveFilename;
- module._resolveFilename = function (filename) {
- if (!filename.match(/node-sass/)) {
- return originalResolve.apply(this, arguments);
- }
- const err = new Error("Some error");
+ module._resolveFilename = function (filename) {
+ if (!filename.match(/node-sass/)) {
+ return originalResolve.apply(this, arguments);
+ }
+ const err = new Error("Some error");
- err.code = "MODULE_NOT_FOUND";
- throw err;
- };
- runWebpack({
- entry: pathToSassLoader + "!" + pathToErrorFile
- }, (err) => {
- module._resolveFilename = originalResolve;
- mockRequire.reRequire("node-sass");
- err.message.should.match(/Some error/);
- done();
- });
- });
- it("should output a message when `node-sass` is an incompatible version", (done) => {
- mockRequire.reRequire(pathToSassLoader);
- mockRequire("node-sass/package.json", { version: "3.0.0" });
- runWebpack({
- entry: pathToSassLoader + "!" + pathToErrorFile
- }, (err) => {
- mockRequire.stop("node-sass");
- err.message.should.match(/The installed version of `node-sass` is not compatible/);
- done();
+ err.code = "MODULE_NOT_FOUND";
+ throw err;
+ };
+ runWebpack({
+ entry: pathToSassLoader + "!" + pathToErrorFile
+ }, { implementation: null }, (err) => {
+ module._resolveFilename = originalResolve;
+ mockRequire.reRequire("node-sass");
+ err.message.should.match(/Some error/);
+ done();
+ });
+ });
+ it("should output a message when the Sass info is unparseable", (done) => {
+ mockRequire.reRequire(pathToSassLoader);
+ runWebpack({
+ entry: pathToErrorFile
+ }, {
+ implementation: merge(nodeSass, { info: "asdfj" })
+ }, (err) => {
+ err.message.should.match(/Unknown Sass implementation "asdfj"\./);
+ done();
+ });
+ });
+ it("should output a message when the Sass version is unparseable", (done) => {
+ mockRequire.reRequire(pathToSassLoader);
+ runWebpack({
+ entry: pathToErrorFile
+ }, {
+ implementation: merge(nodeSass, { info: "node-sass\t1" })
+ }, (err) => {
+ err.message.should.match(/Invalid Sass version "1"\./);
+ done();
+ });
+ });
+ it("should output a message when Node Sass is an incompatible version", (done) => {
+ mockRequire.reRequire(pathToSassLoader);
+ runWebpack({
+ entry: pathToErrorFile
+ }, {
+ implementation: merge(nodeSass, { info: "node-sass\t3.0.0" })
+ }, (err) => {
+ err.message.should.match(/Node Sass version 3\.0\.0 is incompatible with \^4\.0\.0\./);
+ done();
+ });
+ });
+ it("should output a message when Dart Sass is an incompatible version", (done) => {
+ mockRequire.reRequire(pathToSassLoader);
+ runWebpack({
+ entry: pathToErrorFile
+ }, {
+ implementation: merge(nodeSass, { info: "dart-sass\t1.2.0" })
+ }, (err) => {
+ err.message.should.match(/Dart Sass version 1\.2\.0 is incompatible with \^1\.3\.0\./);
+ done();
+ });
+ });
+ it("should output a message for an unknown sass implementation", (done) => {
+ mockRequire.reRequire(pathToSassLoader);
+ runWebpack({
+ entry: pathToErrorFile
+ }, {
+ implementation: merge(nodeSass, { info: "strange-sass\t1.0.0" })
+ }, (err) => {
+ err.message.should.match(/Unknown Sass implementation "strange-sass"\./);
+ done();
+ });
+ });
});
});
- });
-});
-
-function readCss(ext, id) {
- return fs.readFileSync(path.join(__dirname, ext, "spec", id + ".css"), "utf8").replace(CR, "");
-}
-function runWebpack(baseConfig, done) {
- const webpackConfig = merge({
- mode: "development",
- output: {
- path: path.join(__dirname, "output"),
- filename: "bundle.js",
- libraryTarget: "commonjs2"
+ function readCss(ext, id) {
+ return fs.readFileSync(path.join(__dirname, ext, "spec", implementationName, id + ".css"), "utf8").replace(CR, "");
}
- }, baseConfig);
- webpack(webpackConfig, (webpackErr, stats) => {
- const err = webpackErr ||
- (stats.hasErrors() && stats.compilation.errors[0]) ||
- (stats.hasWarnings() && stats.compilation.warnings[0]);
+ function runWebpack(baseConfig, loaderOptions, done) {
+ const webpackConfig = merge({
+ mode: "development",
+ output: {
+ path: path.join(__dirname, "output"),
+ filename: "bundle.js",
+ libraryTarget: "commonjs2"
+ },
+ module: {
+ rules: [{
+ test: /\.s[ac]ss$/,
+ use: [
+ { loader: "raw-loader" },
+ {
+ loader: pathToSassLoader,
+ options: merge({ implementation }, loaderOptions)
+ }
+ ]
+ }]
+ }
+ }, baseConfig);
+
+ webpack(webpackConfig, (webpackErr, stats) => {
+ const err = webpackErr ||
+ (stats.hasErrors() && stats.compilation.errors[0]) ||
+ (stats.hasWarnings() && stats.compilation.warnings[0]);
- done(err || null);
+ done(err || null);
+ });
+ }
});
-}
+});
function readBundle(filename) {
delete require.cache[path.resolve(__dirname, `./output/${ filename }`)];
diff --git a/test/node_modules/animate.css/animate.css b/test/node_modules/animate.css/animate.css
deleted file mode 100644
index 6ff1c8d0..00000000
--- a/test/node_modules/animate.css/animate.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.animate-css {
- background: hotpink;
-}
diff --git a/test/sass/import-from-npm-org-pkg.sass b/test/sass/import-from-npm-org-pkg.sass
index 43c22271..9fc088eb 100644
--- a/test/sass/import-from-npm-org-pkg.sass
+++ b/test/sass/import-from-npm-org-pkg.sass
@@ -4,4 +4,4 @@
@import ~@org/bar/foo
.foo
- background: #000;
+ background: #000
diff --git a/test/sass/import-include-paths.sass b/test/sass/import-include-paths.sass
index d6b7c510..9cbcb635 100644
--- a/test/sass/import-include-paths.sass
+++ b/test/sass/import-include-paths.sass
@@ -1,3 +1,2 @@
@import include-path-module
@import underscore-include-path-module
-@import animate.css/animate
diff --git a/test/sass/imports.sass b/test/sass/imports.sass
index 2fb9b4fc..1094133a 100644
--- a/test/sass/imports.sass
+++ b/test/sass/imports.sass
@@ -10,13 +10,10 @@
// @see https://github.com/webpack-contrib/sass-loader/issues/167
/* @import ~sass/some.module */
@import ~sass/some.module
-// @see https://github.com/webpack-contrib/sass-loader/issues/360
-/* @import ~animate.css/animate */
-@import ~animate.css/animate
/* @import url(http://example.com/something/from/the/interwebs); */
-@import url(http://example.com/something/from/the/interwebs);
+@import url(http://example.com/something/from/the/interwebs)
/* scoped import @import language */
-.scoped-imporr
+.scoped-import
@import language
// The local util file should take precedence over Node's util module
diff --git a/test/sass/includePath/animate.css/animate.css b/test/sass/includePath/animate.css/animate.css
deleted file mode 100644
index 6ff1c8d0..00000000
--- a/test/sass/includePath/animate.css/animate.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.animate-css {
- background: hotpink;
-}
diff --git a/test/sass/spec/dart-sass/.gitignore b/test/sass/spec/dart-sass/.gitignore
new file mode 100644
index 00000000..db8b4be7
--- /dev/null
+++ b/test/sass/spec/dart-sass/.gitignore
@@ -0,0 +1,2 @@
+# Will be populated by npm pretest
+*.css
diff --git a/test/sass/spec/node-sass/.gitignore b/test/sass/spec/node-sass/.gitignore
new file mode 100644
index 00000000..db8b4be7
--- /dev/null
+++ b/test/sass/spec/node-sass/.gitignore
@@ -0,0 +1,2 @@
+# Will be populated by npm pretest
+*.css
diff --git a/test/scss/import-include-paths.scss b/test/scss/import-include-paths.scss
index 23adb601..15b70814 100644
--- a/test/scss/import-include-paths.scss
+++ b/test/scss/import-include-paths.scss
@@ -1,3 +1,2 @@
@import "include-path-module";
@import "underscore-include-path-module";
-@import "animate.css/animate";
diff --git a/test/scss/imports.scss b/test/scss/imports.scss
index 8de13d79..a55e52e1 100644
--- a/test/scss/imports.scss
+++ b/test/scss/imports.scss
@@ -10,9 +10,6 @@
// @see https://github.com/webpack-contrib/sass-loader/issues/167
/* @import "~scss/some.module"; */
@import "~scss/some.module";
-// @see https://github.com/webpack-contrib/sass-loader/issues/360
-/* @import "~animate.css/animate"; */
-@import "~animate.css/animate";
/* @import url(http://example.com/something/from/the/interwebs); */
@import url(http://example.com/something/from/the/interwebs);
/* scoped import @import "language"; */
diff --git a/test/scss/includePath/animate.css/animate.css b/test/scss/includePath/animate.css/animate.css
deleted file mode 100644
index 6ff1c8d0..00000000
--- a/test/scss/includePath/animate.css/animate.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.animate-css {
- background: hotpink;
-}
diff --git a/test/scss/spec/dart-sass/.gitignore b/test/scss/spec/dart-sass/.gitignore
new file mode 100644
index 00000000..db8b4be7
--- /dev/null
+++ b/test/scss/spec/dart-sass/.gitignore
@@ -0,0 +1,2 @@
+# Will be populated by npm pretest
+*.css
diff --git a/test/scss/spec/node-sass/.gitignore b/test/scss/spec/node-sass/.gitignore
new file mode 100644
index 00000000..db8b4be7
--- /dev/null
+++ b/test/scss/spec/node-sass/.gitignore
@@ -0,0 +1,2 @@
+# Will be populated by npm pretest
+*.css
diff --git a/test/tools/createSpec.js b/test/tools/createSpec.js
index dcb20edb..8f004476 100644
--- a/test/tools/createSpec.js
+++ b/test/tools/createSpec.js
@@ -1,12 +1,14 @@
"use strict";
-const sass = require("node-sass");
+const nodeSass = require("node-sass");
+const dartSass = require("sass");
const os = require("os");
const fs = require("fs");
const path = require("path");
const customImporter = require("./customImporter.js");
const customFunctions = require("./customFunctions.js");
+const implementations = [nodeSass, dartSass];
const testFolder = path.resolve(__dirname, "../");
const error = "error";
@@ -44,7 +46,6 @@ function createSpec(ext) {
file: url
};
},
- functions: customFunctions,
includePaths: [
path.join(testFolder, ext, "another"),
path.join(testFolder, ext, "includePath")
@@ -52,15 +53,28 @@ function createSpec(ext) {
};
if (/prepending-data/.test(fileName)) {
- sassOptions.data = "$prepended-data: hotpink;" + os.EOL + fs.readFileSync(fileName, "utf8");
sassOptions.indentedSyntax = /\.sass$/.test(fileName);
+ sassOptions.data = "$prepended-data: hotpink" + (sassOptions.indentedSyntax ? "\n" : ";") +
+ os.EOL + fs.readFileSync(fileName, "utf8");
} else {
sassOptions.file = fileName;
}
- const css = sass.renderSync(sassOptions).css;
+ implementations.forEach(implementation => {
+ if (fileWithoutExt === "import-css" && implementation !== nodeSass) {
+ // Skip CSS imports for all implementations that are not node-sass
+ // CSS imports is a legacy feature that we only support for node-sass
+ // See discussion https://github.com/webpack-contrib/sass-loader/pull/573/files?#r199109203
+ return;
+ }
- fs.writeFileSync(path.join(basePath, "spec", fileWithoutExt + ".css"), css, "utf8");
+ sassOptions.functions = customFunctions(implementation);
+
+ const name = implementation.info.split("\t")[0];
+ const css = implementation.renderSync(sassOptions).css;
+
+ fs.writeFileSync(path.join(basePath, "spec", name, fileWithoutExt + ".css"), css, "utf8");
+ });
});
}
diff --git a/test/tools/customFunctions.js b/test/tools/customFunctions.js
index 968321c7..acb8b308 100644
--- a/test/tools/customFunctions.js
+++ b/test/tools/customFunctions.js
@@ -1,18 +1,18 @@
"use strict";
-const sass = require("node-sass");
+module.exports = function (implementation) {
+ return {
+ "headings($from: 0, $to: 6)"(from, to) {
+ const f = from.getValue();
+ const t = to.getValue();
+ const list = new implementation.types.List(t - f + 1);
+ let i;
-module.exports = {
- "headings($from: 0, $to: 6)"(from, to) {
- const f = from.getValue();
- const t = to.getValue();
- const list = new sass.types.List(t - f + 1);
- let i;
+ for (i = f; i <= t; i++) {
+ list.setValue(i - f, new implementation.types.String("h" + i));
+ }
- for (i = f; i <= t; i++) {
- list.setValue(i - f, new sass.types.String("h" + i));
+ return list;
}
-
- return list;
- }
+ };
};