From 6439cef04b342be6787e6fe3182655af3abff877 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Thu, 12 Apr 2018 18:13:56 -0400 Subject: [PATCH] feat: Remove `node-sass` from `peerDependencies` (#533) - Only require("node-sass") on runtime when loader has been called - Based on feedback from https://github.com/webpack-contrib/sass-loader/pull/533 and https://github.com/facebook/create-react-app/pull/4195 BREAKING CHANGE: The sass-loader throws an error at runtime now and refuses to compile if the peer dependency is wrong. This could break applications where npm's peer dependency warning was just ignored. --- .nycrc | 4 ++-- lib/loader.js | 25 +++++++++++++++++++++++-- package-lock.json | 10 ++++++++++ package.json | 2 +- test/index.test.js | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 5 deletions(-) diff --git a/.nycrc b/.nycrc index d0117319..666465eb 100644 --- a/.nycrc +++ b/.nycrc @@ -7,8 +7,8 @@ "lib/**/*.js" ], "lines": 97, - "statements": 91, + "statements": 97, "functions": 100, - "branches": 89, + "branches": 91, "check-coverage": true } diff --git a/lib/loader.js b/lib/loader.js index 0a1fcd7f..b56668ff 100644 --- a/lib/loader.js +++ b/lib/loader.js @@ -1,6 +1,5 @@ "use strict"; -const sass = require("node-sass"); const path = require("path"); const async = require("neo-async"); const formatSassError = require("./formatSassError"); @@ -12,7 +11,7 @@ const pify = require("pify"); // 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; -const asyncSassJobQueue = async.queue(sass.render, threadPoolSize - 1); +let asyncSassJobQueue = null; /** * The sass-loader makes node-sass available to webpack modules. @@ -21,6 +20,28 @@ const asyncSassJobQueue = async.queue(sass.render, threadPoolSize - 1); * @param {string} content */ function sassLoader(content) { + if (asyncSassJobQueue === null) { + let sass; + let sassVersion; + + try { + sass = require("node-sass"); + sassVersion = /^(\d+)/.exec(require("node-sass/package.json").version).pop(); + } catch (e) { + throw new Error( + "`sass-loader` requires `node-sass` >=4. Please install a compatible version." + ); + } + + 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; diff --git a/package-lock.json b/package-lock.json index 7771f987..afb0baff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5290,6 +5290,16 @@ } } }, + "mock-require": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.2.tgz", + "integrity": "sha512-aD/Y1ZFHqw5pHg3HVQ50dLbfaAAcytS6sqLuhP51Dk3TSPdFb2VkSAa3mjrHifLIlGAtwQHJHINafAyqAne7vA==", + "dev": true, + "requires": { + "get-caller-file": "1.0.2", + "normalize-path": "2.1.1" + } + }, "modify-values": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.0.tgz", diff --git a/package.json b/package.json index 51fa22ae..0ee04d49 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "eslint-plugin-jsdoc": "^2.4.0", "file-loader": "^0.11.2", "mocha": "^3.0.2", + "mock-require": "^3.0.1", "node-sass": "^4.5.0", "nyc": "^11.0.2", "raw-loader": "^0.5.1", @@ -54,7 +55,6 @@ "node": ">= 6.9.0 || >= 8.9.0" }, "peerDependencies": { - "node-sass": "^4.0.0", "webpack": "^3.0.0 || ^4.0.0" }, "keywords": [ diff --git a/test/index.test.js b/test/index.test.js index 15096c96..9797f16c 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -11,6 +11,7 @@ const customFunctions = require("./tools/customFunctions.js"); const pathToSassLoader = require.resolve("../lib/loader.js"); const testLoader = require("./tools/testLoader"); const sassLoader = require(pathToSassLoader); +const mockRequire = require("mock-require"); const CR = /\r/g; const syntaxStyles = ["scss", "sass"]; @@ -256,6 +257,40 @@ describe("sass-loader", () => { done(); }); }); + it("should output a message when `node-sass` is missing", (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(); + + err.code = "MODULE_NOT_FOUND"; + throw err; + }; + runWebpack({ + entry: pathToSassLoader + "!" + pathToErrorFile + }, (err) => { + module._resolveFilename = originalResolve; + mockRequire.reRequire("node-sass"); + err.message.should.match(/Please install a compatible version/); + 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(); + }); + }); }); });