diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 4bcd226e..1f10acb4 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -55,7 +55,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [10.x, 12.x, 13.x] + node-version: [10.x, 12.x, 14.x] webpack-version: [latest, next] runs-on: ${{ matrix.os }} diff --git a/src/LessError.js b/src/LessError.js new file mode 100644 index 00000000..db610dd8 --- /dev/null +++ b/src/LessError.js @@ -0,0 +1,33 @@ +class LessError extends Error { + constructor(error) { + super(); + + this.message = [ + '\n', + ...LessError.getFileExcerptIfPossible(error), + error.message.charAt(0).toUpperCase() + error.message.slice(1), + ` Error in ${error.filename} (line ${error.line}, column ${error.column})`, + ].join('\n'); + + this.hideStack = true; + } + + static getFileExcerptIfPossible(lessErr) { + if (lessErr.extract === 'undefined') { + return []; + } + + const excerpt = lessErr.extract.slice(0, 2); + const column = Math.max(lessErr.column - 1, 0); + + if (typeof excerpt[0] === 'undefined') { + excerpt.shift(); + } + + excerpt.push(`${new Array(column).join(' ')}^`); + + return excerpt; + } +} + +export default LessError; diff --git a/src/createWebpackLessPlugin.js b/src/createWebpackLessPlugin.js index d19559f2..d46b3bd7 100644 --- a/src/createWebpackLessPlugin.js +++ b/src/createWebpackLessPlugin.js @@ -98,15 +98,16 @@ function createWebpackLessPlugin(loaderContext) { result = await super.loadFile(filename, ...args); } catch (error) { if (error.type !== 'File' && error.type !== 'Next') { - loaderContext.emitError(error); - return Promise.reject(error); } try { result = await this.resolveFilename(filename, ...args); - } catch (e) { - loaderContext.emitError(e); + } catch (webpackResolveError) { + error.message = + `Less resolver error:\n${error.message}\n\n` + + `Webpack resolver error details:\n${webpackResolveError.details}\n\n` + + `Webpack resolver error missing:\n${webpackResolveError.missing}\n\n`; return Promise.reject(error); } diff --git a/src/formatLessError.js b/src/formatLessError.js deleted file mode 100644 index bbfd832c..00000000 --- a/src/formatLessError.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * 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 {LessError} err - * @returns {Array} - */ -function getFileExcerptIfPossible(lessErr) { - try { - const excerpt = lessErr.extract.slice(0, 2); - const column = Math.max(lessErr.column - 1, 0); - - if (typeof excerpt[0] === 'undefined') { - excerpt.shift(); - } - - excerpt.push(`${new Array(column).join(' ')}^`); - - return excerpt; - } catch (unexpectedErr) { - // If anything goes wrong here, we don't want any errors to be reported to the user - return []; - } -} - -/** - * Beautifies the error message from Less. - * - * @param {LessError} lessErr - * @param {string} lessErr.type - e.g. 'Name' - * @param {string} lessErr.message - e.g. '.undefined-mixin is undefined' - * @param {string} lessErr.filename - e.g. '/path/to/style.less' - * @param {number} lessErr.index - e.g. 352 - * @param {number} lessErr.line - e.g. 31 - * @param {number} lessErr.callLine - e.g. NaN - * @param {string} lessErr.callExtract - e.g. undefined - * @param {number} lessErr.column - e.g. 6 - * @param {Array} lessErr.extract - e.g. [' .my-style {', ' .undefined-mixin;', ' display: block;'] - * @returns {LessError} - */ -function formatLessError(err) { - /* eslint-disable no-param-reassign */ - const msg = err.message; - - // Instruct webpack to hide the JS stack from the console - // Usually you're only interested in the SASS stack in this case. - err.hideStack = true; - - err.message = [ - '\n', - ...getFileExcerptIfPossible(err), - msg.charAt(0).toUpperCase() + msg.slice(1), - ` in ${err.filename} (line ${err.line}, column ${err.column})`, - ].join('\n'); - - return err; -} /* eslint-enable no-param-reassign */ - -module.exports = formatLessError; diff --git a/src/getLessOptions.js b/src/getLessOptions.js index c68c548c..1af03c6b 100644 --- a/src/getLessOptions.js +++ b/src/getLessOptions.js @@ -7,30 +7,9 @@ import createWebpackLessPlugin from './createWebpackLessPlugin'; * * @param {object} loaderContext * @param {object} loaderOptions - * @param {string} content * @returns {Object} */ -function getLessOptions(loaderContext, loaderOptions, content) { - function prependData(target, addedData) { - if (!addedData) { - return target; - } - - return typeof addedData === 'function' - ? `${addedData(loaderContext)}\n${target}` - : `${addedData}\n${target}`; - } - - function appendData(target, addedData) { - if (!addedData) { - return target; - } - - return typeof addedData === 'function' - ? `${target}\n${addedData(loaderContext)}` - : `${target}\n${addedData}`; - } - +function getLessOptions(loaderContext, loaderOptions) { const options = clone( loaderOptions.lessOptions ? typeof loaderOptions.lessOptions === 'function' @@ -39,17 +18,12 @@ function getLessOptions(loaderContext, loaderOptions, content) { : {} ); - let data = content; - data = prependData(data, loaderOptions.prependData); - data = appendData(data, loaderOptions.appendData); - const lessOptions = { plugins: [], relativeUrls: true, // We need to set the filename because otherwise our WebpackFileManager will receive an undefined path for the entry filename: loaderContext.resourcePath, ...options, - data, }; lessOptions.plugins.push(createWebpackLessPlugin(loaderContext)); diff --git a/src/index.js b/src/index.js index 16661436..e6f95155 100644 --- a/src/index.js +++ b/src/index.js @@ -5,8 +5,7 @@ import validateOptions from 'schema-utils'; import schema from './options.json'; import getLessOptions from './getLessOptions'; -import removeSourceMappingUrl from './removeSourceMappingUrl'; -import formatLessError from './formatLessError'; +import LessError from './LessError'; function lessLoader(source) { const options = getOptions(this); @@ -17,28 +16,48 @@ function lessLoader(source) { }); const callback = this.async(); - const lessOptions = getLessOptions(this, options, source); + const lessOptions = getLessOptions(this, options); + + let data = source; + data = prependData(data, options.prependData); + data = appendData(data, options.appendData); less - .render(lessOptions.data, lessOptions) + .render(data, lessOptions) .then(({ css, map, imports }) => { imports.forEach(this.addDependency, this); // Removing the sourceMappingURL comment. // See removeSourceMappingUrl.js for the reasoning behind this. - callback( - null, - removeSourceMappingUrl(css), - typeof map === 'string' ? JSON.parse(map) : map - ); + callback(null, css, typeof map === 'string' ? JSON.parse(map) : map); }) .catch((lessError) => { if (lessError.filename) { this.addDependency(lessError.filename); } - callback(formatLessError(lessError)); + callback(new LessError(lessError)); }); + + function prependData(target, addedData) { + if (!addedData) { + return target; + } + + return typeof addedData === 'function' + ? `${addedData(this)}\n${target}` + : `${addedData}\n${target}`; + } + + function appendData(target, addedData) { + if (!addedData) { + return target; + } + + return typeof addedData === 'function' + ? `${target}\n${addedData(this)}` + : `${target}\n${addedData}`; + } } export default lessLoader; diff --git a/src/removeSourceMappingUrl.js b/src/removeSourceMappingUrl.js deleted file mode 100644 index 460e8f81..00000000 --- a/src/removeSourceMappingUrl.js +++ /dev/null @@ -1,17 +0,0 @@ -const matchSourceMappingUrl = /\/\*# sourceMappingURL=[^*]+\*\//; - -/** - * Removes the sourceMappingURL comment. This is necessary because the less-loader - * does not know where the final source map will be located. Thus, we remove every - * reference to source maps. In a regular setup, the css-loader will embed the - * source maps into the CommonJS module and the style-loader will translate it into - * base64 blob urls. - * - * @param {string} content - * @returns {string} - */ -function removeSourceMappingUrl(content) { - return content.replace(matchSourceMappingUrl, ''); -} - -module.exports = removeSourceMappingUrl; diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index b1b1e73c..bf41bf2b 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -166,8 +166,6 @@ exports[`loader should provide a useful error message if the import could not be Array [ "ModuleBuildError: Module build failed (from \`replaced original path\`): ", - "ModuleError: Module Error (from \`replaced original path\`): -Can't resolve 'not-existing.less' in '/test/fixtures'", ] `;