diff --git a/src/SassError.js b/src/SassError.js new file mode 100644 index 00000000..d8dfd2a0 --- /dev/null +++ b/src/SassError.js @@ -0,0 +1,30 @@ +class SassError extends Error { + constructor(sassError, resourcePath) { + super(); + + this.name = 'SassError'; + this.originalSassError = sassError; + this.loc = { + line: sassError.line, + column: sassError.column, + }; + + // Keep original error if `sassError.formatted` is unavailable + this.message = `${this.name}: ${this.originalSassError.message}`; + + if (this.originalSassError.formatted) { + this.message = `${this.name}: ${this.originalSassError.formatted + .replace(/^Error: /, '') + .replace(/(\s*)stdin(\s*)/, `$1${resourcePath}$2`)}`; + + // Instruct webpack to hide the JS stack from the console. + // Usually you're only interested in the SASS stack in this case. + // eslint-disable-next-line no-param-reassign + this.hideStack = true; + + Error.captureStackTrace(this, this.constructor); + } + } +} + +export default SassError; diff --git a/src/formatSassError.js b/src/formatSassError.js deleted file mode 100644 index d9788b4d..00000000 --- a/src/formatSassError.js +++ /dev/null @@ -1,78 +0,0 @@ -import path from 'path'; -import os from 'os'; -import fs from 'fs'; - -// A typical sass error looks like this -// const SassError = { -// message: "invalid property name", -// column: 14, -// line: 1, -// file: "stdin", -// status: 1 -// }; - -/** - * Enhances the sass error with additional information about what actually went wrong. - * - * @param {SassError} error - * @param {string} resourcePath - */ -function formatSassError(error, resourcePath) { - // Instruct webpack to hide the JS stack from the console - // Usually you're only interested in the SASS stack in this case. - // eslint-disable-next-line no-param-reassign - error.hideStack = true; - - // The file property is missing in rare cases. - // No improvement in the error is possible. - if (!error.file) { - return; - } - - let msg = error.message; - - if (error.file === 'stdin') { - // eslint-disable-next-line no-param-reassign - error.file = resourcePath; - } - - // node-sass returns UNIX-style paths - // eslint-disable-next-line no-param-reassign - error.file = path.normalize(error.file); - - // The 'Current dir' hint of node-sass does not help us, we're providing - // additional information by reading the err.file property - msg = msg.replace(/\s*Current dir:\s*/, ''); - // msg = msg.replace(/(\s*)(stdin)(\s*)/, `$1${err.file}$3`); - - // eslint-disable-next-line no-param-reassign - error.message = `${getFileExcerptIfPossible(error) + - msg.charAt(0).toUpperCase() + - msg.slice(1) + - os.EOL} in ${error.file} (line ${error.line}, column ${error.column})`; -} - -/** - * Tries to get an excerpt of the file where the error happened. - * Uses err.line and err.column. - * - * Returns an empty string if the excerpt could not be retrieved. - * - * @param {SassError} error - * @returns {string} - */ -function getFileExcerptIfPossible(error) { - try { - const content = fs.readFileSync(error.file, 'utf8'); - - return `${os.EOL + - content.split(/\r?\n/)[error.line - 1] + - os.EOL + - new Array(error.column - 1).join(' ')}^${os.EOL} `; - } catch (ignoreError) { - // If anything goes wrong here, we don't want any errors to be reported to the user - return ''; - } -} - -export default formatSassError; diff --git a/src/getDefaultSassImplementation.js b/src/getDefaultSassImplementation.js new file mode 100644 index 00000000..ebb35955 --- /dev/null +++ b/src/getDefaultSassImplementation.js @@ -0,0 +1,19 @@ +function getDefaultSassImplementation() { + let sassImplPkg = 'node-sass'; + + try { + require.resolve('node-sass'); + } catch (error) { + try { + require.resolve('sass'); + sassImplPkg = 'sass'; + } catch (ignoreError) { + sassImplPkg = 'node-sass'; + } + } + + // eslint-disable-next-line import/no-dynamic-require, global-require + return require(sassImplPkg); +} + +export default getDefaultSassImplementation; diff --git a/src/getRenderFunctionFromSassImplementation.js b/src/getRenderFunctionFromSassImplementation.js new file mode 100644 index 00000000..0facd785 --- /dev/null +++ b/src/getRenderFunctionFromSassImplementation.js @@ -0,0 +1,64 @@ +import semver from 'semver'; +import async from 'neo-async'; + +let nodeSassJobQueue = null; + +/** + * Verifies that the implementation and version of Sass is supported by this loader. + * + * @param {Object} module + * @returns {Function} + */ +function getRenderFunctionFromSassImplementation(module) { + const { info } = module; + + if (!info) { + throw new Error('Unknown Sass implementation.'); + } + + const components = info.split('\t'); + + if (components.length < 2) { + throw new Error(`Unknown Sass implementation "${info}".`); + } + + const [implementation, version] = components; + + 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}".`); +} + +export default getRenderFunctionFromSassImplementation; diff --git a/src/getSassOptions.js b/src/getSassOptions.js index 1fb4f584..7c1c9328 100644 --- a/src/getSassOptions.js +++ b/src/getSassOptions.js @@ -89,9 +89,9 @@ function getSassOptions(loaderContext, loaderOptions, content) { ? proxyCustomImporters(options.importer, resourcePath) : []; - // `node-sass` uses `includePaths` to resolve `@import` paths. Append the currently processed file. - options.includePaths = options.includePaths || []; - options.includePaths.push(path.dirname(resourcePath)); + options.includePaths = (options.includePaths || []).concat( + path.dirname(resourcePath) + ); return options; } diff --git a/src/importsToResolve.js b/src/importsToResolve.js index 1f40b6de..a3bf1b6e 100644 --- a/src/importsToResolve.js +++ b/src/importsToResolve.js @@ -24,7 +24,7 @@ function importsToResolve(url) { const request = utils.urlToRequest(url); // Keep in mind: ext can also be something like '.datepicker' when the true extension is omitted and the filename contains a dot. // @see https://github.com/webpack-contrib/sass-loader/issues/167 - const ext = path.extname(request); + const ext = path.extname(request).toLowerCase(); // In case there is module request, send this to webpack resolver if (matchModuleImport.test(url)) { diff --git a/src/index.js b/src/index.js index 21028b1f..cf47eed7 100644 --- a/src/index.js +++ b/src/index.js @@ -1,16 +1,14 @@ import path from 'path'; import validateOptions from 'schema-utils'; -import async from 'neo-async'; -import semver from 'semver'; import { getOptions } from 'loader-utils'; import schema from './options.json'; -import formatSassError from './formatSassError'; -import webpackImporter from './webpackImporter'; import getSassOptions from './getSassOptions'; - -let nodeSassJobQueue = null; +import webpackImporter from './webpackImporter'; +import getDefaultSassImplementation from './getDefaultSassImplementation'; +import getRenderFunctionFromSassImplementation from './getRenderFunctionFromSassImplementation'; +import SassError from './SassError'; /** * The sass-loader makes node-sass and dart-sass available to webpack modules. @@ -57,19 +55,18 @@ function loader(content) { return; } - const render = getRenderFuncFromSassImpl( - options.implementation || getDefaultSassImpl() + const render = getRenderFunctionFromSassImplementation( + options.implementation || getDefaultSassImplementation() ); render(sassOptions, (error, result) => { if (error) { - formatSassError(error, this.resourcePath); - if (error.file) { this.dependency(error.file); } - callback(error); + callback(new SassError(error, this.resourcePath)); + return; } @@ -117,80 +114,4 @@ function loader(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; - - if (!info) { - throw new Error('Unknown Sass implementation.'); - } - - const components = info.split('\t'); - - if (components.length < 2) { - throw new Error(`Unknown Sass implementation "${info}".`); - } - - const [implementation, version] = components; - - 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}".`); -} - -function getDefaultSassImpl() { - let sassImplPkg = 'node-sass'; - - try { - require.resolve('node-sass'); - } catch (error) { - try { - require.resolve('sass'); - sassImplPkg = 'sass'; - } catch (ignoreError) { - sassImplPkg = 'node-sass'; - } - } - - // eslint-disable-next-line import/no-dynamic-require, global-require - return require(sassImplPkg); -} - export default loader; diff --git a/src/webpackImporter.js b/src/webpackImporter.js index 2d09a88b..543283b3 100644 --- a/src/webpackImporter.js +++ b/src/webpackImporter.js @@ -18,7 +18,7 @@ import path from 'path'; import importsToResolve from './importsToResolve'; -const matchCss = /\.css$/; +const matchCss = /\.css$/i; /** * Returns an importer that uses webpack's resolving algorithm. diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index 5c16c09d..8be312d4 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -3,16 +3,12 @@ exports[`loader should output an understandable error (dart-sass) (sass): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): - -a { - ^ - Expected newline. +SassError: Expected newline. ╷ 1 │ a { │ ^ ╵ - stdin 1:3 root stylesheet - in absolute_path_to_file (line 1, column 3)], + /absolute/path/to/sass/error.sass 1:3 root stylesheet], ] `; @@ -21,16 +17,12 @@ exports[`loader should output an understandable error (dart-sass) (sass): warnin exports[`loader should output an understandable error (dart-sass) (scss): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): - - some-value - ^ - Expected "{". +SassError: expected "{". ╷ 2 │ some-value │ ^ ╵ - stdin 2:15 root stylesheet - in absolute_path_to_file (line 2, column 15)], + /absolute/path/to/scss/error.scss 2:15 root stylesheet], ] `; @@ -39,11 +31,12 @@ exports[`loader should output an understandable error (dart-sass) (scss): warnin exports[`loader should output an understandable error (node-sass) (sass): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): +SassError: Invalid CSS after "a {": expected "}", was "{}" + on line 1 of /absolute/path/to/sass/error.sass +>> a { {} -a { - ^ - Invalid CSS after "a {": expected "}", was "{}" - in absolute_path_to_file (line 1, column 4)], + ---^ +], ] `; @@ -52,11 +45,12 @@ exports[`loader should output an understandable error (node-sass) (sass): warnin exports[`loader should output an understandable error (node-sass) (scss): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): +SassError: property "some-value" must be followed by a ':' + on line 2 of /absolute/path/to/scss/error.scss +>> some-value - some-value - ^ - Property "some-value" must be followed by a ':' - in absolute_path_to_file (line 2, column 5)], + ----^ +], ] `; @@ -65,16 +59,12 @@ exports[`loader should output an understandable error (node-sass) (scss): warnin exports[`loader should output an understandable error when a file could not be found (dart-sass) (sass): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): - -@import "does-not-exist" - ^ - Can't find stylesheet to import. +SassError: Can't find stylesheet to import. ╷ 1 │ @import "does-not-exist" │ ^^^^^^^^^^^^^^^^ ╵ - stdin 1:9 root stylesheet - in absolute_path_to_file (line 1, column 9)], + /absolute/path/to/sass/error-file-not-found.sass 1:9 root stylesheet], ] `; @@ -83,16 +73,12 @@ exports[`loader should output an understandable error when a file could not be f exports[`loader should output an understandable error when a file could not be found (dart-sass) (scss): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): - -@import "does-not-exist"; - ^ - Can't find stylesheet to import. +SassError: Can't find stylesheet to import. ╷ 1 │ @import "does-not-exist"; │ ^^^^^^^^^^^^^^^^ ╵ - stdin 1:9 root stylesheet - in absolute_path_to_file (line 1, column 9)], + /absolute/path/to/scss/error-file-not-found.scss 1:9 root stylesheet], ] `; @@ -101,11 +87,12 @@ exports[`loader should output an understandable error when a file could not be f exports[`loader should output an understandable error when a file could not be found (node-sass) (sass): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): +SassError: File to import not found or unreadable: does-not-exist. + on line 1 of /absolute/path/to/sass/error-file-not-found.sass +>> @import "does-not-exist"; -@import "does-not-exist" -^ - File to import not found or unreadable: does-not-exist. - in absolute_path_to_file (line 1, column 1)], + ^ +], ] `; @@ -114,11 +101,12 @@ exports[`loader should output an understandable error when a file could not be f exports[`loader should output an understandable error when a file could not be found (node-sass) (scss): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): +SassError: File to import not found or unreadable: does-not-exist. + on line 1 of /absolute/path/to/scss/error-file-not-found.scss +>> @import "does-not-exist"; -@import "does-not-exist"; -^ - File to import not found or unreadable: does-not-exist. - in absolute_path_to_file (line 1, column 1)], + ^ +], ] `; @@ -127,17 +115,13 @@ exports[`loader should output an understandable error when a file could not be f exports[`loader should output an understandable error when the problem in "@import" (dart-sass) (sass): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): - -a { - ^ - Expected newline. +SassError: Expected newline. ╷ 1 │ a { │ ^ ╵ test/sass/error.sass 1:3 @import - stdin 1:9 root stylesheet - in absolute_path_to_file (line 1, column 3)], + /absolute/path/to/sass/error-import.sass 1:9 root stylesheet], ] `; @@ -146,17 +130,13 @@ exports[`loader should output an understandable error when the problem in "@impo exports[`loader should output an understandable error when the problem in "@import" (dart-sass) (scss): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): - - some-value - ^ - Expected "{". +SassError: expected "{". ╷ 2 │ some-value │ ^ ╵ test/scss/error.scss 2:15 @import - stdin 1:9 root stylesheet - in absolute_path_to_file (line 2, column 15)], + /absolute/path/to/scss/error-import.scss 1:9 root stylesheet], ] `; @@ -165,11 +145,13 @@ exports[`loader should output an understandable error when the problem in "@impo exports[`loader should output an understandable error when the problem in "@import" (node-sass) (sass): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): +SassError: Invalid CSS after "a {": expected "}", was "{}" + on line 1 of test/sass/error.sass + from line 1 of /absolute/path/to/sass/error-import.sass +>> a { {} -a { - ^ - Invalid CSS after "a {": expected "}", was "{}" - in absolute_path_to_file (line 1, column 4)], + ---^ +], ] `; @@ -178,11 +160,13 @@ exports[`loader should output an understandable error when the problem in "@impo exports[`loader should output an understandable error when the problem in "@import" (node-sass) (scss): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): +SassError: property "some-value" must be followed by a ':' + on line 2 of test/scss/error.scss + from line 1 of /absolute/path/to/scss/error-import.scss +>> some-value - some-value - ^ - Property "some-value" must be followed by a ':' - in absolute_path_to_file (line 2, column 5)], + ----^ +], ] `; @@ -191,16 +175,12 @@ exports[`loader should output an understandable error when the problem in "@impo exports[`loader should throw an error with a explicit file and a file does not exist (dart-sass) (sass): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): - -@import "./another/_module.scss" - ^ - Can't find stylesheet to import. +SassError: Can't find stylesheet to import. ╷ 1 │ @import "./another/_module.scss" │ ^^^^^^^^^^^^^^^^^^^^^^^^ ╵ - stdin 1:9 root stylesheet - in absolute_path_to_file (line 1, column 9)], + /absolute/path/to/sass/error-file-not-found-2.sass 1:9 root stylesheet], ] `; @@ -209,16 +189,12 @@ exports[`loader should throw an error with a explicit file and a file does not e exports[`loader should throw an error with a explicit file and a file does not exist (dart-sass) (scss): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): - -@import "./another/_module.scss"; - ^ - Can't find stylesheet to import. +SassError: Can't find stylesheet to import. ╷ 1 │ @import "./another/_module.scss"; │ ^^^^^^^^^^^^^^^^^^^^^^^^ ╵ - stdin 1:9 root stylesheet - in absolute_path_to_file (line 1, column 9)], + /absolute/path/to/scss/error-file-not-found-2.scss 1:9 root stylesheet], ] `; @@ -227,11 +203,12 @@ exports[`loader should throw an error with a explicit file and a file does not e exports[`loader should throw an error with a explicit file and a file does not exist (node-sass) (sass): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): +SassError: File to import not found or unreadable: ./another/_module.scss. + on line 1 of /absolute/path/to/sass/error-file-not-found-2.sass +>> @import "./another/_module.scss"; -@import "./another/_module.scss" -^ - File to import not found or unreadable: ./another/_module.scss. - in absolute_path_to_file (line 1, column 1)], + ^ +], ] `; @@ -240,11 +217,12 @@ exports[`loader should throw an error with a explicit file and a file does not e exports[`loader should throw an error with a explicit file and a file does not exist (node-sass) (scss): errors 1`] = ` Array [ [ModuleBuildError: Module build failed (from ../src/cjs.js): +SassError: File to import not found or unreadable: ./another/_module.scss. + on line 1 of /absolute/path/to/scss/error-file-not-found-2.scss +>> @import "./another/_module.scss"; -@import "./another/_module.scss"; -^ - File to import not found or unreadable: ./another/_module.scss. - in absolute_path_to_file (line 1, column 1)], + ^ +], ] `; diff --git a/test/helpers/normalizeError.js b/test/helpers/normalizeError.js index 22e9cc3c..746994dc 100644 --- a/test/helpers/normalizeError.js +++ b/test/helpers/normalizeError.js @@ -1,14 +1,14 @@ +import path from 'path'; + function normalizeError(error) { // eslint-disable-next-line no-param-reassign - error.message = error.message.replace(/\\/gm, '/'); - // eslint-disable-next-line no-param-reassign - error.message = error.message.replace(/\r\n/gm, '\n'); + error.message = error.message.replace(/\sat.*/g, ' at ReplacedStackEntry'); // eslint-disable-next-line no-param-reassign - error.message = error.message.replace(/\sat.*/gm, ' at ReplacedStackEntry'); + error.message = error.message.replace(/\\/g, '/'); // eslint-disable-next-line no-param-reassign error.message = error.message.replace( - / in .*? /g, - ' in absolute_path_to_file ' + new RegExp(path.resolve(__dirname, '..').replace(/\\/g, '/'), 'g'), + '/absolute/path/to' ); return error; diff --git a/test/manual/webpack.config.js b/test/manual/webpack.config.js index 53d49db3..1499a6b9 100644 --- a/test/manual/webpack.config.js +++ b/test/manual/webpack.config.js @@ -21,8 +21,11 @@ module.exports = { { loader: require.resolve('../../dist/cjs.js'), options: { - // eslint-disable-next-line global-require - implementation: require('sass'), + implementation: process.env.SASS_IMPLEMENTATION + ? // eslint-disable-next-line global-require, import/no-dynamic-require + require(process.env.SASS_IMPLEMENTATION) + : // eslint-disable-next-line global-require + require('sass'), sourceMap: true, }, }, @@ -33,5 +36,6 @@ module.exports = { devServer: { hot: true, contentBase: __dirname, + overlay: true, }, };