├── test ├── fixtures │ ├── empty.less │ ├── error.less │ ├── 3rd │ │ └── b.less │ ├── broken.less │ ├── foo.php │ ├── import-paths.less │ ├── node_modules │ │ ├── package-with-exports │ │ │ ├── index.cjs │ │ │ ├── index.js │ │ │ ├── style.less │ │ │ └── package.json │ │ ├── some-illegal-2 │ │ │ ├── index.js │ │ │ ├── index.less │ │ │ └── package.json │ │ ├── some-illegal │ │ │ ├── module.js │ │ │ ├── module.less │ │ │ └── package.json │ │ ├── package │ │ │ └── style.less │ │ ├── prefer-relative │ │ │ └── index.less │ │ ├── less-package-1 │ │ │ ├── index.less │ │ │ └── package.json │ │ ├── some │ │ │ ├── css.css │ │ │ ├── module.less │ │ │ └── package.json │ │ ├── @scope │ │ │ ├── css.css │ │ │ └── module.less │ │ ├── package-with-exports-and-custom-condition │ │ │ ├── index.js │ │ │ ├── index.cjs │ │ │ ├── style-1.less │ │ │ ├── style-2.less │ │ │ └── package.json │ │ └── less-package-2 │ │ │ ├── package.json │ │ │ ├── node_modules │ │ │ └── less-package-1 │ │ │ │ ├── index.less │ │ │ │ └── package.json │ │ │ └── index.less │ ├── watch.less │ ├── warn.less │ ├── glob.less │ ├── implementation-error.js │ ├── import-absolute.less │ ├── import-dependency.less │ ├── import-non-less-2.less │ ├── import-non-less.less │ ├── import-with-css-extension.less │ ├── import-with-extension.less │ ├── import-with-php-extension.less │ ├── import-without-extension.less │ ├── css.css │ ├── error-import-not-existing.less │ ├── import-absolute-2.less │ ├── import-absolute-3.less │ ├── custom-main-files │ │ └── custom.less │ ├── error-import-file-with-error.less │ ├── folder │ │ ├── some.file │ │ ├── nested.less │ │ ├── url-path.less │ │ ├── customImportPlugin.js │ │ └── customFileLoaderPlugin.js │ ├── import-package-with-exports.less │ ├── prefer-relative │ │ └── index.less │ ├── additional-data.less │ ├── circular.less │ ├── import-absolute-target.less │ ├── file-load-replacement.less │ ├── data-uri.less │ ├── by-dependency.less │ ├── less-package.less │ ├── resolve-working-directory │ │ ├── resolve-working-directory-a.less │ │ └── index.less │ ├── import-nested.less │ ├── basic-plugins-2.less │ ├── basic-plugins.less │ ├── import-relative.less │ ├── error-syntax.less │ ├── import-package-with-exports-and-custom-condition.less │ ├── img.less │ ├── import-prefer-relative.less │ ├── import-webpack-js-package-2.less │ ├── import-webpack-js-package.less │ ├── err.less │ ├── logging.less │ ├── source-map.less │ ├── import-scope.less │ ├── error-mixed-resolvers.less │ ├── file-load.less │ ├── import-url-deps.less │ ├── resources │ │ └── circle.svg │ ├── import-webpack-alias.less │ ├── mock-fonts.less │ ├── url-path.less │ ├── import.less │ ├── import-url.less │ ├── import-webpack-aliases.less │ ├── import-webpack.less │ ├── plugin-1.js │ ├── plugin-2.js │ ├── basic.less │ └── import-keyword-url.less ├── helpers │ ├── getErrors.js │ ├── getWarnings.js │ ├── compile.js │ ├── testLoader.js │ ├── readAssets.js │ ├── validateDependencies.js │ ├── execute.js │ ├── normalizeErrors.js │ ├── readAsset.js │ ├── index.js │ ├── getCodeFromBundle.js │ ├── getCompiler.js │ └── getCodeFromLess.js ├── cjs.test.js ├── __snapshots__ │ ├── additionalData-option.test.js.snap │ ├── implementation.test.js.snap │ ├── webpackImporter-options.test.js.snap │ ├── sourceMap-options.test.js.snap │ ├── validate-options.test.js.snap │ └── loader.test.js.snap ├── implementation.test.js ├── validate-options.test.js ├── additionalData-option.test.js ├── webpackImporter-options.test.js ├── sourceMap-options.test.js └── loader.test.js ├── .husky ├── pre-commit └── commit-msg ├── src ├── cjs.js ├── options.json ├── index.js └── utils.js ├── .prettierignore ├── .gitattributes ├── lint-staged.config.js ├── commitlint.config.js ├── eslint.config.mjs ├── .editorconfig ├── babel.config.js ├── .github └── workflows │ ├── dependency-review.yml │ └── nodejs.yml ├── .gitignore ├── .cspell.json ├── LICENSE ├── package.json ├── README.md └── CHANGELOG.md /test/fixtures/empty.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | lint-staged 2 | -------------------------------------------------------------------------------- /test/fixtures/error.less: -------------------------------------------------------------------------------- 1 | broken -------------------------------------------------------------------------------- /test/fixtures/3rd/b.less: -------------------------------------------------------------------------------- 1 | a{color:red} -------------------------------------------------------------------------------- /test/fixtures/broken.less: -------------------------------------------------------------------------------- 1 | broken; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | commitlint --edit $1 2 | -------------------------------------------------------------------------------- /test/fixtures/foo.php: -------------------------------------------------------------------------------- 1 | a { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/import-paths.less: -------------------------------------------------------------------------------- 1 | @import "module.less"; 2 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/package-with-exports/index.cjs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/watch.less: -------------------------------------------------------------------------------- 1 | @import 'package/style.less'; 2 | -------------------------------------------------------------------------------- /src/cjs.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./index").default; 2 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/package-with-exports/index.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/warn.less: -------------------------------------------------------------------------------- 1 | div { 2 | &:extend(.body1); 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/glob.less: -------------------------------------------------------------------------------- 1 | @import "custom-main-files/**.less"; 2 | -------------------------------------------------------------------------------- /test/fixtures/implementation-error.js: -------------------------------------------------------------------------------- 1 | module.exports = false; 2 | -------------------------------------------------------------------------------- /test/fixtures/import-absolute.less: -------------------------------------------------------------------------------- 1 | @import "@{absolutePath}"; 2 | -------------------------------------------------------------------------------- /test/fixtures/import-dependency.less: -------------------------------------------------------------------------------- 1 | @import "some/module.less"; 2 | -------------------------------------------------------------------------------- /test/fixtures/import-non-less-2.less: -------------------------------------------------------------------------------- 1 | @import "../../some.file"; 2 | -------------------------------------------------------------------------------- /test/fixtures/import-non-less.less: -------------------------------------------------------------------------------- 1 | @import "folder/some.file"; 2 | -------------------------------------------------------------------------------- /test/fixtures/import-with-css-extension.less: -------------------------------------------------------------------------------- 1 | @import "css.css"; 2 | -------------------------------------------------------------------------------- /test/fixtures/import-with-extension.less: -------------------------------------------------------------------------------- 1 | @import "basic.less"; 2 | -------------------------------------------------------------------------------- /test/fixtures/import-with-php-extension.less: -------------------------------------------------------------------------------- 1 | @import "foo.php"; 2 | -------------------------------------------------------------------------------- /test/fixtures/import-without-extension.less: -------------------------------------------------------------------------------- 1 | @import "basic"; 2 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/some-illegal-2/index.js: -------------------------------------------------------------------------------- 1 | // Some code 2 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/some-illegal/module.js: -------------------------------------------------------------------------------- 1 | // Some code 2 | -------------------------------------------------------------------------------- /test/fixtures/css.css: -------------------------------------------------------------------------------- 1 | .classical-css { 2 | background: hotpink; 3 | } -------------------------------------------------------------------------------- /test/fixtures/error-import-not-existing.less: -------------------------------------------------------------------------------- 1 | @import "~not-existing"; 2 | -------------------------------------------------------------------------------- /test/fixtures/import-absolute-2.less: -------------------------------------------------------------------------------- 1 | @import "/fixtures/basic.less"; 2 | -------------------------------------------------------------------------------- /test/fixtures/import-absolute-3.less: -------------------------------------------------------------------------------- 1 | @import "/styles/style.less"; 2 | -------------------------------------------------------------------------------- /test/fixtures/custom-main-files/custom.less: -------------------------------------------------------------------------------- 1 | .a { 2 | color: red 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/error-import-file-with-error.less: -------------------------------------------------------------------------------- 1 | @import "./error-syntax"; 2 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/package/style.less: -------------------------------------------------------------------------------- 1 | a { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/folder/some.file: -------------------------------------------------------------------------------- 1 | .some-file { 2 | background: hotpink; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/import-package-with-exports.less: -------------------------------------------------------------------------------- 1 | @import 'package-with-exports'; 2 | -------------------------------------------------------------------------------- /test/fixtures/prefer-relative/index.less: -------------------------------------------------------------------------------- 1 | .relative { 2 | color: coral; 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /test/fixtures 5 | CHANGELOG.md -------------------------------------------------------------------------------- /test/fixtures/additional-data.less: -------------------------------------------------------------------------------- 1 | .background { 2 | color: @background; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/circular.less: -------------------------------------------------------------------------------- 1 | @import "circular"; 2 | 3 | a { 4 | color: red; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/folder/nested.less: -------------------------------------------------------------------------------- 1 | .nested-import { 2 | background: coral; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/import-absolute-target.less: -------------------------------------------------------------------------------- 1 | .it-works { 2 | color: yellow; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/file-load-replacement.less: -------------------------------------------------------------------------------- 1 | .file-loader { 2 | background: coral; 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | bin/* eol=lf 3 | yarn.lock -diff 4 | package-lock.json -diff 5 | -------------------------------------------------------------------------------- /test/fixtures/data-uri.less: -------------------------------------------------------------------------------- 1 | .img { 2 | background: data-uri("./resources/circle.svg"); 3 | } -------------------------------------------------------------------------------- /test/fixtures/node_modules/package-with-exports/style.less: -------------------------------------------------------------------------------- 1 | .load-me { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/prefer-relative/index.less: -------------------------------------------------------------------------------- 1 | .not-relative { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/by-dependency.less: -------------------------------------------------------------------------------- 1 | @import "custom-main-files"; 2 | 3 | .b { 4 | color: red 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/less-package.less: -------------------------------------------------------------------------------- 1 | @import "less-package-2"; 2 | 3 | .top { 4 | color: red; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/less-package-1/index.less: -------------------------------------------------------------------------------- 1 | .less-package-1 { 2 | background: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/some/css.css: -------------------------------------------------------------------------------- 1 | .modules-dir-some-module { 2 | background: hotpink; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/some/module.less: -------------------------------------------------------------------------------- 1 | .modules-dir-some-module { 2 | color: hotpink; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/@scope/css.css: -------------------------------------------------------------------------------- 1 | .modules-dir-scope-module { 2 | background: hotpink; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/@scope/module.less: -------------------------------------------------------------------------------- 1 | .modules-dir-scope-module { 2 | color: hotpink; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/some/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "some", 3 | "main": "module.less" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/resolve-working-directory/resolve-working-directory-a.less: -------------------------------------------------------------------------------- 1 | .test { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/some-illegal-2/index.less: -------------------------------------------------------------------------------- 1 | .modules-dir-some-module { 2 | color: hotpink; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/some-illegal/module.less: -------------------------------------------------------------------------------- 1 | .modules-dir-some-module { 2 | color: hotpink; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/import-nested.less: -------------------------------------------------------------------------------- 1 | .top-import { 2 | background: red; 3 | } 4 | 5 | @import "folder/nested.less"; 6 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/package-with-exports-and-custom-condition/index.js: -------------------------------------------------------------------------------- 1 | console.log('Some js, clearly not sass'); -------------------------------------------------------------------------------- /test/fixtures/basic-plugins-2.less: -------------------------------------------------------------------------------- 1 | @plugin "plugin-2"; 2 | 3 | .webpackLoaderContext { 4 | isDefined: run(); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/basic-plugins.less: -------------------------------------------------------------------------------- 1 | @plugin "plugin-1"; 2 | 3 | .webpackLoaderContext { 4 | isDefined: run(); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/import-relative.less: -------------------------------------------------------------------------------- 1 | .top-import { 2 | background: red; 3 | } 4 | 5 | @import "folder/url-path.less"; 6 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/less-package-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "less-package-1", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/less-package-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "less-package-2", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/package-with-exports-and-custom-condition/index.cjs: -------------------------------------------------------------------------------- 1 | console.log('Some js, clearly not sass'); -------------------------------------------------------------------------------- /test/fixtures/node_modules/package-with-exports-and-custom-condition/style-1.less: -------------------------------------------------------------------------------- 1 | .load-me { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/package-with-exports-and-custom-condition/style-2.less: -------------------------------------------------------------------------------- 1 | .load-me { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/some-illegal-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "some-illegal-2", 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/some-illegal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "some-illegal", 3 | "main": "module.js" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/error-syntax.less: -------------------------------------------------------------------------------- 1 | .this-less-code-works { 2 | background: hotpink; 3 | } 4 | 5 | but this is a syntax error 6 | -------------------------------------------------------------------------------- /test/fixtures/import-package-with-exports-and-custom-condition.less: -------------------------------------------------------------------------------- 1 | @import 'package-with-exports-and-custom-condition'; 2 | -------------------------------------------------------------------------------- /test/fixtures/img.less: -------------------------------------------------------------------------------- 1 | .img { 2 | background: url(some/img.jpg); 3 | } 4 | .img2 { 5 | background: url(../img.jpg); 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/import-prefer-relative.less: -------------------------------------------------------------------------------- 1 | .prefer-relative-import { 2 | background: red; 3 | } 4 | 5 | @import "preferAlias"; 6 | -------------------------------------------------------------------------------- /test/fixtures/import-webpack-js-package-2.less: -------------------------------------------------------------------------------- 1 | @import "~some-illegal-2"; 2 | 3 | .some-class { 4 | background: hotpink; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/import-webpack-js-package.less: -------------------------------------------------------------------------------- 1 | @import "~some-illegal"; 2 | 3 | .some-class { 4 | background: hotpink; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/err.less: -------------------------------------------------------------------------------- 1 | div { 2 | .m(@x) when (default()) {} 3 | .m(@x) when not(default()) {} 4 | 5 | .m(1); // Error 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/logging.less: -------------------------------------------------------------------------------- 1 | @import (optional) "foo.less"; 2 | 3 | nav ul { 4 | &:extend(.inline); 5 | background: blue; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/less-package-2/node_modules/less-package-1/index.less: -------------------------------------------------------------------------------- 1 | .less-package-1-nested { 2 | background: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/source-map.less: -------------------------------------------------------------------------------- 1 | @import "some/module"; 2 | 3 | #it-works:extend(.modules-dir-some-module) { 4 | margin: 10px; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/less-package-2/index.less: -------------------------------------------------------------------------------- 1 | @import "less-package-1/index.less"; 2 | 3 | .less-package-2 { 4 | background: red; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/less-package-2/node_modules/less-package-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "less-package-1", 3 | "version": "2.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /test/helpers/getErrors.js: -------------------------------------------------------------------------------- 1 | import normalizeErrors from "./normalizeErrors"; 2 | 3 | export default (stats) => normalizeErrors(stats.compilation.errors.sort()); 4 | -------------------------------------------------------------------------------- /test/fixtures/resolve-working-directory/index.less: -------------------------------------------------------------------------------- 1 | @import './resolve-working-directory-a.less'; 2 | @import '3rd/b.less'; 3 | 4 | body { 5 | margin: 0; 6 | } 7 | -------------------------------------------------------------------------------- /test/helpers/getWarnings.js: -------------------------------------------------------------------------------- 1 | import normalizeErrors from "./normalizeErrors"; 2 | 3 | export default (stats) => normalizeErrors(stats.compilation.warnings.sort()); 4 | -------------------------------------------------------------------------------- /test/fixtures/import-scope.less: -------------------------------------------------------------------------------- 1 | @import "~@scope/module"; 2 | @import "~@scope/css.css"; 3 | 4 | #it-works:extend(.modules-dir-scope-module) { 5 | margin: 10px; 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/error-mixed-resolvers.less: -------------------------------------------------------------------------------- 1 | // You can't use include paths and webpack's resolver simultaneously. 2 | @import "some/module.less"; 3 | @import "~some/module.less"; 4 | -------------------------------------------------------------------------------- /test/fixtures/file-load.less: -------------------------------------------------------------------------------- 1 | @import "forFileLoaderPluginResolve.less"; 2 | @import (once) url("https://fonts.googleapis.com/css?family=Roboto:500"); 3 | @import "@scope/module"; 4 | -------------------------------------------------------------------------------- /test/fixtures/import-url-deps.less: -------------------------------------------------------------------------------- 1 | @import url("http://fonts.googleapis.com/css?family=Roboto:300"); 2 | @import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500"); 3 | -------------------------------------------------------------------------------- /test/fixtures/resources/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/cjs.test.js: -------------------------------------------------------------------------------- 1 | import src from "../src"; 2 | import cjs from "../src/cjs"; 3 | 4 | describe("cjs", () => { 5 | it("should exported", () => { 6 | expect(cjs).toEqual(src); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /test/fixtures/folder/url-path.less: -------------------------------------------------------------------------------- 1 | .img4 { 2 | background: url(img.jpg); 3 | } 4 | .img5 { 5 | background: url(some/img.jpg); 6 | } 7 | .img6 { 8 | background: url(../img.jpg); 9 | } 10 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*": [ 3 | "prettier --cache --write --ignore-unknown", 4 | "cspell --cache --no-must-find-files", 5 | ], 6 | "*.js": ["eslint --cache --fix"], 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/import-webpack-alias.less: -------------------------------------------------------------------------------- 1 | @import "~some"; // test if package.json's are correctly resolved 2 | 3 | .some-class { 4 | background: hotpink; 5 | } 6 | 7 | @import "~aliased-some"; // should also resolve to ~some 8 | -------------------------------------------------------------------------------- /test/fixtures/mock-fonts.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | font-style: normal; 4 | font-weight: 500; 5 | src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmEU9fBBc9.ttf) format('truetype'); 6 | } -------------------------------------------------------------------------------- /test/fixtures/url-path.less: -------------------------------------------------------------------------------- 1 | @import "folder/url-path"; 2 | 3 | .img1 { 4 | background: url(img.jpg); 5 | } 6 | .img2 { 7 | background: url(some/img.jpg); 8 | } 9 | .img3 { 10 | background: url(../img.jpg); 11 | } 12 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | rules: { 4 | "header-max-length": [0], 5 | "body-max-line-length": [0], 6 | "footer-max-line-length": [0], 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import configs from "eslint-config-webpack/configs.js"; 3 | 4 | export default defineConfig([ 5 | { 6 | extends: [configs["recommended-dirty"]], 7 | }, 8 | ]); 9 | -------------------------------------------------------------------------------- /test/fixtures/import.less: -------------------------------------------------------------------------------- 1 | @import "css.css"; 2 | @import (css) "css.css"; 3 | @import (less) "css.css"; 4 | @import (inline) "css.css"; 5 | @import "basic"; 6 | 7 | #it-works:extend(.box, .classical-css) { 8 | margin: 10px; 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /test/fixtures/import-url.less: -------------------------------------------------------------------------------- 1 | @import (css) url("http://fonts.googleapis.com/css?family=Roboto:300,400,500"); 2 | @import (css) url("https://fonts.googleapis.com/css?family=Roboto:300,400,500"); 3 | @import (css) url("//fonts.googleapis.com/css?family=Roboto:300,400,500"); 4 | -------------------------------------------------------------------------------- /test/helpers/compile.js: -------------------------------------------------------------------------------- 1 | export default (compiler) => 2 | new Promise((resolve, reject) => { 3 | compiler.run((error, stats) => { 4 | if (error) { 5 | return reject(error); 6 | } 7 | 8 | return resolve(stats); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /test/fixtures/import-webpack-aliases.less: -------------------------------------------------------------------------------- 1 | @import "~fileAlias"; 2 | @import "assets/basic.less"; 3 | @import "assets/basic.less"; 4 | 5 | body { 6 | background: url(assets/resources/circle.svg); 7 | } 8 | 9 | .abs { 10 | background: url(assets/resources/circle.svg); 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/import-webpack.less: -------------------------------------------------------------------------------- 1 | @import (reference) "~some/module"; 2 | @import "some/css.css"; 3 | @import (css) "some/css.css"; 4 | @import (less) "~some/css.css"; 5 | @import (inline) "some/css.css"; 6 | 7 | #it-works:extend(.modules-dir-some-module) { 8 | margin: 10px; 9 | } 10 | -------------------------------------------------------------------------------- /test/helpers/testLoader.js: -------------------------------------------------------------------------------- 1 | function testLoader(content, sourceMap) { 2 | const result = { css: content }; 3 | 4 | if (sourceMap) { 5 | result.map = sourceMap; 6 | } 7 | 8 | return `export default ${JSON.stringify(result)}`; 9 | } 10 | 11 | module.exports = testLoader; 12 | -------------------------------------------------------------------------------- /test/fixtures/plugin-1.js: -------------------------------------------------------------------------------- 1 | registerPlugin({ 2 | install: function(less, pluginManager, functions) { 3 | functions.add('run', function() { 4 | if (typeof less.webpackLoaderContext !== 'undefined') { 5 | return 'true'; 6 | } 7 | 8 | return 'false'; 9 | }); 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/fixtures/plugin-2.js: -------------------------------------------------------------------------------- 1 | registerPlugin({ 2 | install: function(less, pluginManager, functions) { 3 | functions.add('run', function() { 4 | if (typeof pluginManager.webpackLoaderContext !== 'undefined') { 5 | return 'true'; 6 | } 7 | 8 | return 'false'; 9 | }); 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /test/helpers/readAssets.js: -------------------------------------------------------------------------------- 1 | import readAsset from "./readAsset"; 2 | 3 | export default function readAssets(compiler, stats) { 4 | const assets = {}; 5 | 6 | for (const asset of Object.keys(stats.compilation.assets)) { 7 | assets[asset] = readAsset(asset, compiler, stats); 8 | } 9 | 10 | return assets; 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/folder/customImportPlugin.js: -------------------------------------------------------------------------------- 1 | class PluginPreProcessor { 2 | process() { 3 | return '.imported-class {color: coral;}' 4 | } 5 | } 6 | 7 | class CustomImportPlugin { 8 | install(less, pluginManager) { 9 | pluginManager.addPreProcessor(new PluginPreProcessor()); 10 | } 11 | } 12 | 13 | module.exports = CustomImportPlugin; 14 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const MIN_BABEL_VERSION = 7; 2 | 3 | module.exports = (api) => { 4 | api.assertVersion(MIN_BABEL_VERSION); 5 | api.cache(true); 6 | 7 | return { 8 | presets: [ 9 | [ 10 | "@babel/preset-env", 11 | { 12 | targets: { 13 | node: "18.12.0", 14 | }, 15 | }, 16 | ], 17 | ], 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: "Dependency Review" 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | dependency-review: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: "Checkout Repository" 12 | uses: actions/checkout@v5 13 | - name: "Dependency Review" 14 | uses: actions/dependency-review-action@v4 15 | -------------------------------------------------------------------------------- /test/helpers/validateDependencies.js: -------------------------------------------------------------------------------- 1 | const illegalSymbol = process.platform === "win32" ? "/" : "\\"; 2 | 3 | export default (dependencies) => { 4 | for (const item of dependencies) { 5 | if (item.includes(illegalSymbol)) { 6 | throw new Error( 7 | `The file path "${item}" should not contain "${illegalSymbol}"`, 8 | ); 9 | } 10 | } 11 | 12 | return true; 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/basic.less: -------------------------------------------------------------------------------- 1 | @base: #f938ab; 2 | 3 | .box-shadow(@style, @c) when (iscolor(@c)) { 4 | -webkit-box-shadow: @style @c; 5 | box-shadow: @style @c; 6 | } 7 | .box-shadow(@style, @alpha: 50%) when (isnumber(@alpha)) { 8 | .box-shadow(@style, rgba(0, 0, 0, @alpha)); 9 | } 10 | .box { 11 | color: saturate(@base, 5%); 12 | border-color: lighten(@base, 30%); 13 | div { .box-shadow(0 0 5px, 30%) } 14 | background: url(box.png); 15 | } 16 | -------------------------------------------------------------------------------- /test/helpers/execute.js: -------------------------------------------------------------------------------- 1 | import Module from "node:module"; 2 | import path from "node:path"; 3 | 4 | const parentModule = module; 5 | 6 | export default (code) => { 7 | const resource = "test.js"; 8 | const module = new Module(resource, parentModule); 9 | 10 | module.paths = Module._nodeModulePaths( 11 | path.resolve(__dirname, "../fixtures"), 12 | ); 13 | module.filename = resource; 14 | 15 | module._compile(code, resource); 16 | 17 | return module.exports; 18 | }; 19 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/package-with-exports/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-with-exports", 3 | "version": "1.0.0", 4 | "description": "test", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "", 9 | "license": "MIT", 10 | "type": "module", 11 | "module": "index.js", 12 | "main": "index.cjs", 13 | "less": "style.less", 14 | "exports": { 15 | "require": "./index.cjs", 16 | "import": "./index.js", 17 | "less": "./style.less" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/import-keyword-url.less: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:300,400'); 2 | @import (inline) url("https://fonts.googleapis.com/css?family=Roboto:300"); 3 | @import (less) url("https://fonts.googleapis.com/css?family=Roboto:400"); 4 | @import (once) url("https://fonts.googleapis.com/css?family=Roboto:500"); 5 | @import (multiple) url("https://fonts.googleapis.com/css?family=Roboto:700"); 6 | 7 | // This url not allowed (should be ignored) 8 | @import (optional) url("https://fonts.googleapis.com/css?family=Roboto:600"); 9 | -------------------------------------------------------------------------------- /test/helpers/normalizeErrors.js: -------------------------------------------------------------------------------- 1 | function removeCWD(str) { 2 | const isWin = process.platform === "win32"; 3 | let cwd = process.cwd(); 4 | 5 | if (isWin) { 6 | str = str.replaceAll("\\", "/"); 7 | 8 | cwd = cwd.replaceAll("\\", "/"); 9 | } 10 | 11 | return str 12 | .replace(/\(from .*?\)/, "(from `replaced original path`)") 13 | .replaceAll(new RegExp(cwd, "g"), ""); 14 | } 15 | 16 | export default (errors) => 17 | errors.map((error) => 18 | removeCWD(error.toString().split("\n").slice(0, 2).join("\n")), 19 | ); 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | pids 10 | logs 11 | results 12 | npm-debug.log 13 | /node_modules 14 | /coverage 15 | .idea 16 | .nyc_output 17 | npm-debug.log* 18 | yarn-debug.log* 19 | .eslintcache 20 | .cspellcache 21 | /coverage 22 | /dist 23 | /local 24 | /reports 25 | /test/fixtures/css 26 | /test/fixtures/generated-1.less 27 | /test/fixtures/generated-2.less 28 | /test/fixtures/generated-3.less 29 | /test/output 30 | .DS_Store 31 | Thumbs.db 32 | .vscode 33 | *.sublime-project 34 | *.sublime-workspace 35 | *.iml 36 | -------------------------------------------------------------------------------- /test/helpers/readAsset.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | export default (asset, compiler, stats) => { 4 | const usedFs = compiler.outputFileSystem; 5 | const outputPath = stats.compilation.outputOptions.path; 6 | 7 | let data = ""; 8 | let targetFile = asset; 9 | 10 | const queryStringIdx = targetFile.indexOf("?"); 11 | 12 | if (queryStringIdx >= 0) { 13 | targetFile = targetFile.slice(0, queryStringIdx); 14 | } 15 | 16 | try { 17 | data = usedFs.readFileSync(path.join(outputPath, targetFile)).toString(); 18 | } catch (error) { 19 | data = error.toString(); 20 | } 21 | 22 | return data; 23 | }; 24 | -------------------------------------------------------------------------------- /test/fixtures/node_modules/package-with-exports-and-custom-condition/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-with-exports-and-custom-condition", 3 | "version": "1.0.0", 4 | "description": "test", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "", 9 | "license": "MIT", 10 | "type": "module", 11 | "module": "index.js", 12 | "main": "index.cjs", 13 | "sass": "style-1.scss", 14 | "exports": { 15 | "require": "./index.cjs", 16 | "import": "./index.js", 17 | "less": { 18 | "theme1": "./style-1.less", 19 | "theme2": "./style-2.less" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "language": "en,en-gb", 4 | "words": [ 5 | "memfs", 6 | "truetype", 7 | "googleapis", 8 | "hotpink", 9 | "isnumber", 10 | "iscolor", 11 | "klona", 12 | "FOUC", 13 | "sourcemaps", 14 | "commitlint" 15 | ], 16 | 17 | "ignorePaths": [ 18 | "CHANGELOG.md", 19 | "package.json", 20 | "dist/**", 21 | "**/__snapshots__/**", 22 | "package-lock.json", 23 | "/test/fixtures/generated-1.less", 24 | "/test/fixtures/generated-2.less", 25 | "/test/fixtures/generated-3.less", 26 | "node_modules", 27 | "coverage", 28 | "*.log" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | export { default as compile } from "./compile"; 2 | export { default as getCodeFromBundle } from "./getCodeFromBundle"; 3 | export { default as execute } from "./execute"; 4 | export { default as getCompiler } from "./getCompiler"; 5 | export { default as getCodeFromLess } from "./getCodeFromLess"; 6 | export { default as getWarnings } from "./getWarnings"; 7 | export { default as getErrors } from "./getErrors"; 8 | export { default as readAsset } from "./readAsset"; 9 | export { default as normalizeErrors } from "./normalizeErrors"; 10 | export { default as validateDependencies } from "./validateDependencies"; 11 | export { default as readsAssets } from "./readAssets"; 12 | -------------------------------------------------------------------------------- /test/helpers/getCodeFromBundle.js: -------------------------------------------------------------------------------- 1 | import vm from "node:vm"; 2 | 3 | import readAsset from "./readAsset"; 4 | 5 | function getCodeFromBundle(stats, compiler, asset) { 6 | let code = null; 7 | 8 | if ( 9 | stats && 10 | stats.compilation && 11 | stats.compilation.assets && 12 | stats.compilation.assets[asset || "main.bundle.js"] 13 | ) { 14 | code = readAsset(asset || "main.bundle.js", compiler, stats); 15 | } 16 | 17 | if (!code) { 18 | throw new Error("Can't find compiled code"); 19 | } 20 | 21 | const result = vm.runInNewContext( 22 | `${code};\nmodule.exports = lessLoaderExport;`, 23 | { 24 | module: {}, 25 | }, 26 | ); 27 | 28 | return result.__esModule ? result.default : result; 29 | } 30 | 31 | export default getCodeFromBundle; 32 | -------------------------------------------------------------------------------- /test/fixtures/folder/customFileLoaderPlugin.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import less from "less"; 3 | 4 | class Plugin extends less.FileManager { 5 | supports(filename) { 6 | if (filename === 'forFileLoaderPluginResolve.less') { 7 | return true; 8 | } 9 | 10 | if (filename === "https://fonts.googleapis.com/css?family=Roboto:500") { 11 | return true; 12 | } 13 | 14 | return false; 15 | } 16 | 17 | loadFile(filename, ...args) { 18 | let result; 19 | 20 | if (filename === 'forFileLoaderPluginResolve.less') { 21 | result = path.resolve(__dirname, '../', 'file-load-replacement.less'); 22 | } else if (filename === "https://fonts.googleapis.com/css?family=Roboto:500") { 23 | result = path.resolve(__dirname, '../', 'mock-fonts.less'); 24 | } 25 | 26 | return super.loadFile(result, ...args); 27 | }; 28 | } 29 | 30 | class CustomFileLoaderPlugin { 31 | install(less, pluginManager) { 32 | pluginManager.addFileManager(new Plugin()); 33 | } 34 | } 35 | 36 | module.exports = CustomFileLoaderPlugin; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/__snapshots__/additionalData-option.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`"additionalData" option should as function: css 1`] = ` 4 | "/* RelativePath: additional-data.less; */ 5 | .background { 6 | color: coral; 7 | } 8 | .custom-class { 9 | color: red; 10 | } 11 | " 12 | `; 13 | 14 | exports[`"additionalData" option should as function: errors 1`] = `[]`; 15 | 16 | exports[`"additionalData" option should as function: warnings 1`] = `[]`; 17 | 18 | exports[`"additionalData" option should work as async function: css 1`] = ` 19 | "/* RelativePath: additional-data.less; */ 20 | .background { 21 | color: coral; 22 | } 23 | .custom-class { 24 | color: red; 25 | } 26 | " 27 | `; 28 | 29 | exports[`"additionalData" option should work as async function: errors 1`] = `[]`; 30 | 31 | exports[`"additionalData" option should work as async function: warnings 1`] = `[]`; 32 | 33 | exports[`"additionalData" option should work as string: css 1`] = ` 34 | ".background { 35 | color: coral; 36 | } 37 | " 38 | `; 39 | 40 | exports[`"additionalData" option should work as string: errors 1`] = `[]`; 41 | 42 | exports[`"additionalData" option should work as string: warnings 1`] = `[]`; 43 | -------------------------------------------------------------------------------- /test/helpers/getCompiler.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import { Volume, createFsFromVolume } from "memfs"; 4 | import webpack from "webpack"; 5 | 6 | export default (fixture, loaderOptions = {}, config = {}) => { 7 | const fullConfig = { 8 | mode: "development", 9 | devtool: config.devtool || false, 10 | context: path.resolve(__dirname, "../fixtures"), 11 | entry: path.resolve(__dirname, "../fixtures", fixture), 12 | output: { 13 | path: path.resolve(__dirname, "../outputs"), 14 | filename: "[name].bundle.js", 15 | chunkFilename: "[name].chunk.js", 16 | library: "lessLoaderExport", 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.less$/i, 22 | rules: [ 23 | { 24 | loader: require.resolve("./testLoader"), 25 | }, 26 | { 27 | loader: path.resolve(__dirname, "../../src"), 28 | options: loaderOptions || {}, 29 | }, 30 | ], 31 | }, 32 | ], 33 | }, 34 | plugins: [], 35 | ...config, 36 | }; 37 | 38 | const compiler = webpack(fullConfig); 39 | 40 | if (!config.outputFileSystem) { 41 | compiler.outputFileSystem = createFsFromVolume(new Volume()); 42 | } 43 | 44 | return compiler; 45 | }; 46 | -------------------------------------------------------------------------------- /test/__snapshots__/implementation.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing 2 | 3 | exports[`"implementation" option should throw error when implementation has error: errors 1`] = ` 4 | [ 5 | "ModuleBuildError: Module build failed (from \`replaced original path\`): 6 | Error: The Less implementation "/test/fixtures/implementation-error.js" not found", 7 | ] 8 | `; 9 | 10 | exports[`"implementation" option should throw error when implementation has error: warnings 1`] = `[]`; 11 | 12 | exports[`"implementation" option should throw error when unresolved package: errors 1`] = ` 13 | [ 14 | "ModuleBuildError: Module build failed (from \`replaced original path\`): 15 | NonErrorEmittedError: (Emitted value instead of an instance of Error) Error: Cannot find module 'unresolved' from 'src/utils.js'", 16 | ] 17 | `; 18 | 19 | exports[`"implementation" option should throw error when unresolved package: warnings 1`] = `[]`; 20 | 21 | exports[`"implementation" option should work when implementation option is string: css 1`] = ` 22 | ".box { 23 | color: #fe33ac; 24 | border-color: #fdcdea; 25 | background: url(box.png); 26 | } 27 | .box div { 28 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 29 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 30 | } 31 | " 32 | `; 33 | 34 | exports[`"implementation" option should work when implementation option is string: errors 1`] = `[]`; 35 | 36 | exports[`"implementation" option should work when implementation option is string: warnings 1`] = `[]`; 37 | 38 | exports[`"implementation" option should work: css 1`] = ` 39 | ".box { 40 | color: #fe33ac; 41 | border-color: #fdcdea; 42 | background: url(box.png); 43 | } 44 | .box div { 45 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 46 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 47 | } 48 | " 49 | `; 50 | 51 | exports[`"implementation" option should work: errors 1`] = `[]`; 52 | 53 | exports[`"implementation" option should work: warnings 1`] = `[]`; 54 | -------------------------------------------------------------------------------- /src/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Less Loader options", 3 | "type": "object", 4 | "properties": { 5 | "lessOptions": { 6 | "description": "Options to pass through to `Less`.", 7 | "link": "https://github.com/webpack/less-loader#lessoptions", 8 | "anyOf": [ 9 | { 10 | "type": "object", 11 | "additionalProperties": true 12 | }, 13 | { 14 | "instanceof": "Function" 15 | } 16 | ] 17 | }, 18 | "additionalData": { 19 | "description": "Prepends/Appends `Less` code to the actual entry file.", 20 | "link": "https://github.com/webpack/less-loader#additionalData", 21 | "anyOf": [ 22 | { 23 | "type": "string" 24 | }, 25 | { 26 | "instanceof": "Function" 27 | } 28 | ] 29 | }, 30 | "sourceMap": { 31 | "description": "Enables/Disables generation of source maps.", 32 | "link": "https://github.com/webpack/less-loader#sourcemap", 33 | "type": "boolean" 34 | }, 35 | "webpackImporter": { 36 | "description": "Enables/Disables default `webpack` importer.", 37 | "link": "https://github.com/webpack/less-loader#webpackimporter", 38 | "anyOf": [ 39 | { 40 | "type": "boolean" 41 | }, 42 | { 43 | "type": "string", 44 | "enum": ["only"] 45 | } 46 | ] 47 | }, 48 | "implementation": { 49 | "description": "The implementation of the `Less` to be used.", 50 | "link": "https://github.com/webpack/less-loader#implementation", 51 | "anyOf": [ 52 | { 53 | "type": "string" 54 | }, 55 | { 56 | "type": "object" 57 | } 58 | ] 59 | }, 60 | "lessLogAsWarnOrErr": { 61 | "description": "Less warnings and errors will be webpack warnings or errors.", 62 | "link": "https://github.com/webpack/less-loader#lesslogaswarnorerr", 63 | "type": "boolean" 64 | } 65 | }, 66 | "additionalProperties": false 67 | } 68 | -------------------------------------------------------------------------------- /test/implementation.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | compile, 3 | getCodeFromBundle, 4 | getCodeFromLess, 5 | getCompiler, 6 | getErrors, 7 | getWarnings, 8 | } from "./helpers"; 9 | 10 | describe('"implementation" option', () => { 11 | it("should work", async () => { 12 | const testId = "./basic.less"; 13 | const compiler = getCompiler(testId, { 14 | implementation: require("less"), 15 | }); 16 | const stats = await compile(compiler); 17 | const codeFromBundle = getCodeFromBundle(stats, compiler); 18 | const codeFromLess = await getCodeFromLess(testId); 19 | 20 | expect(codeFromBundle.css).toBe(codeFromLess.css); 21 | expect(codeFromBundle.css).toMatchSnapshot("css"); 22 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 23 | expect(getErrors(stats)).toMatchSnapshot("errors"); 24 | }); 25 | 26 | it("should work when implementation option is string", async () => { 27 | const testId = "./basic.less"; 28 | const compiler = getCompiler(testId, { 29 | implementation: require.resolve("less"), 30 | }); 31 | const stats = await compile(compiler); 32 | const codeFromBundle = getCodeFromBundle(stats, compiler); 33 | const codeFromLess = await getCodeFromLess(testId); 34 | 35 | expect(codeFromBundle.css).toBe(codeFromLess.css); 36 | expect(codeFromBundle.css).toMatchSnapshot("css"); 37 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 38 | expect(getErrors(stats)).toMatchSnapshot("errors"); 39 | }); 40 | 41 | it("should throw error when unresolved package", async () => { 42 | const testId = "./basic.less"; 43 | const compiler = getCompiler(testId, { 44 | implementation: "unresolved", 45 | }); 46 | const stats = await compile(compiler); 47 | 48 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 49 | expect(getErrors(stats)).toMatchSnapshot("errors"); 50 | }); 51 | 52 | it("should throw error when implementation has error", async () => { 53 | const testId = "./basic.less"; 54 | const compiler = getCompiler(testId, { 55 | implementation: require.resolve("./fixtures/implementation-error.js"), 56 | }); 57 | const stats = await compile(compiler); 58 | 59 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 60 | expect(getErrors(stats)).toMatchSnapshot("errors"); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/validate-options.test.js: -------------------------------------------------------------------------------- 1 | import { compile, getCompiler } from "./helpers/index"; 2 | 3 | describe("validate options", () => { 4 | const tests = { 5 | lessOptions: { 6 | success: [ 7 | { strictMath: true }, 8 | () => ({ 9 | strictMath: true, 10 | }), 11 | ], 12 | failure: [1, true, false, "test", []], 13 | }, 14 | additionalData: { 15 | success: ["@background: coral;", () => "@background: coral;"], 16 | failure: [1, true, false, /test/, [], {}], 17 | }, 18 | sourceMap: { 19 | success: [true, false], 20 | failure: ["string"], 21 | }, 22 | webpackImporter: { 23 | success: [true, false, "only"], 24 | failure: ["string"], 25 | }, 26 | implementation: { 27 | success: [require("less"), "less"], 28 | failure: [true, false, () => {}, []], 29 | }, 30 | lessLogAsWarnOrErr: { 31 | success: [true, false], 32 | failure: ["string"], 33 | }, 34 | unknown: { 35 | success: [], 36 | failure: [1, true, false, "test", /test/, [], {}, { foo: "bar" }], 37 | }, 38 | }; 39 | 40 | function stringifyValue(value) { 41 | if ( 42 | Array.isArray(value) || 43 | (value && typeof value === "object" && value.constructor === Object) 44 | ) { 45 | return JSON.stringify(value); 46 | } 47 | 48 | return value; 49 | } 50 | 51 | async function createTestCase(key, value, type) { 52 | it(`should ${ 53 | type === "success" ? "successfully validate" : "throw an error on" 54 | } the "${key}" option with "${stringifyValue(value)}" value`, async () => { 55 | const compiler = getCompiler("./basic.less", { 56 | [key]: value, 57 | }); 58 | let stats; 59 | 60 | try { 61 | stats = await compile(compiler); 62 | } finally { 63 | if (type === "success") { 64 | expect(stats.hasErrors()).toBe(false); 65 | } else if (type === "failure") { 66 | const { 67 | compilation: { errors }, 68 | } = stats; 69 | 70 | expect(errors).toHaveLength(1); 71 | expect(() => { 72 | throw new Error(errors[0].error.message); 73 | }).toThrowErrorMatchingSnapshot(); 74 | } 75 | } 76 | }); 77 | } 78 | 79 | for (const [key, values] of Object.entries(tests)) { 80 | for (const type of Object.keys(values)) { 81 | for (const value of values[type]) { 82 | createTestCase(key, value, type); 83 | } 84 | } 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /test/__snapshots__/webpackImporter-options.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`"webpackImporter" option should throw an error on webpack import when value is "false": errors 1`] = ` 4 | [ 5 | "ModuleBuildError: Module build failed (from \`replaced original path\`): 6 | ", 7 | ] 8 | `; 9 | 10 | exports[`"webpackImporter" option should throw an error on webpack import when value is "false": warnings 1`] = `[]`; 11 | 12 | exports[`"webpackImporter" option should work when value is "false": css 1`] = ` 13 | "@import "css.css"; 14 | @import "css.css"; 15 | .classical-css, 16 | #it-works { 17 | background: hotpink; 18 | } 19 | .box, 20 | #it-works { 21 | color: #fe33ac; 22 | border-color: #fdcdea; 23 | background: url(box.png); 24 | } 25 | .box div { 26 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 27 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 28 | } 29 | #it-works { 30 | margin: 10px; 31 | } 32 | " 33 | `; 34 | 35 | exports[`"webpackImporter" option should work when value is "false": errors 1`] = `[]`; 36 | 37 | exports[`"webpackImporter" option should work when value is "false": warnings 1`] = `[]`; 38 | 39 | exports[`"webpackImporter" option should work when value is "only": css 1`] = ` 40 | "@import "some/css.css"; 41 | @import "some/css.css"; 42 | #it-works { 43 | color: hotpink; 44 | } 45 | .modules-dir-some-module, 46 | #it-works { 47 | background: hotpink; 48 | } 49 | #it-works { 50 | margin: 10px; 51 | } 52 | " 53 | `; 54 | 55 | exports[`"webpackImporter" option should work when value is "only": errors 1`] = `[]`; 56 | 57 | exports[`"webpackImporter" option should work when value is "only": warnings 1`] = `[]`; 58 | 59 | exports[`"webpackImporter" option should work when value is "true": css 1`] = ` 60 | "@import "some/css.css"; 61 | @import "some/css.css"; 62 | #it-works { 63 | color: hotpink; 64 | } 65 | .modules-dir-some-module, 66 | #it-works { 67 | background: hotpink; 68 | } 69 | #it-works { 70 | margin: 10px; 71 | } 72 | " 73 | `; 74 | 75 | exports[`"webpackImporter" option should work when value is "true": errors 1`] = `[]`; 76 | 77 | exports[`"webpackImporter" option should work when value is "true": warnings 1`] = `[]`; 78 | 79 | exports[`"webpackImporter" option should work when value is not specify: css 1`] = ` 80 | "@import "some/css.css"; 81 | @import "some/css.css"; 82 | #it-works { 83 | color: hotpink; 84 | } 85 | .modules-dir-some-module, 86 | #it-works { 87 | background: hotpink; 88 | } 89 | #it-works { 90 | margin: 10px; 91 | } 92 | " 93 | `; 94 | 95 | exports[`"webpackImporter" option should work when value is not specify: errors 1`] = `[]`; 96 | 97 | exports[`"webpackImporter" option should work when value is not specify: warnings 1`] = `[]`; 98 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: less-loader 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | branches: 10 | - main 11 | - next 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | lint: 18 | name: Lint - ${{ matrix.os }} - Node v${{ matrix.node-version }} 19 | 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | node-version: [lts/*] 27 | 28 | runs-on: ${{ matrix.os }} 29 | 30 | concurrency: 31 | group: lint-${{ matrix.os }}-v${{ matrix.node-version }}-${{ github.ref }} 32 | cancel-in-progress: true 33 | 34 | steps: 35 | - uses: actions/checkout@v5 36 | with: 37 | fetch-depth: 0 38 | 39 | - name: Use Node.js ${{ matrix.node-version }} 40 | uses: actions/setup-node@v4 41 | with: 42 | node-version: ${{ matrix.node-version }} 43 | cache: "npm" 44 | 45 | - name: Install dependencies 46 | run: npm ci 47 | 48 | - name: Lint 49 | run: npm run lint 50 | 51 | - name: Security audit 52 | run: npm run security 53 | 54 | - name: Validate PR commits with commitlint 55 | if: github.event_name == 'pull_request' 56 | run: npx commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose 57 | 58 | test: 59 | name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }}, Webpack ${{ matrix.webpack-version }} 60 | 61 | strategy: 62 | matrix: 63 | os: [ubuntu-latest, windows-latest, macos-latest] 64 | node-version: [18.x, 20.x, 22.x, 24.x] 65 | webpack-version: [latest] 66 | 67 | runs-on: ${{ matrix.os }} 68 | 69 | concurrency: 70 | group: test-${{ matrix.os }}-v${{ matrix.node-version }}-${{ matrix.webpack-version }}-${{ github.ref }} 71 | cancel-in-progress: true 72 | 73 | steps: 74 | - name: Setup Git 75 | if: matrix.os == 'windows-latest' 76 | run: git config --global core.autocrlf input 77 | 78 | - uses: actions/checkout@v5 79 | 80 | - name: Use Node.js ${{ matrix.node-version }} 81 | uses: actions/setup-node@v4 82 | with: 83 | node-version: ${{ matrix.node-version }} 84 | cache: "npm" 85 | 86 | - name: Install dependencies 87 | run: npm ci 88 | 89 | - name: Install webpack ${{ matrix.webpack-version }} 90 | if: matrix.webpack-version != 'latest' 91 | run: npm i webpack@${{ matrix.webpack-version }} 92 | 93 | - name: Run tests for webpack version ${{ matrix.webpack-version }} 94 | run: npm run test:coverage -- --ci 95 | 96 | - name: Submit coverage data to codecov 97 | uses: codecov/codecov-action@v5 98 | with: 99 | token: ${{ secrets.CODECOV_TOKEN }} 100 | -------------------------------------------------------------------------------- /test/additionalData-option.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | compile, 3 | getCodeFromBundle, 4 | getCodeFromLess, 5 | getCompiler, 6 | getErrors, 7 | getWarnings, 8 | } from "./helpers"; 9 | 10 | describe('"additionalData" option', () => { 11 | it("should work as string", async () => { 12 | const testId = "./additional-data.less"; 13 | const additionalData = "@background: coral;"; 14 | const compiler = getCompiler(testId, { additionalData }); 15 | const stats = await compile(compiler); 16 | const codeFromBundle = getCodeFromBundle(stats, compiler); 17 | const codeFromLess = await getCodeFromLess(testId, { additionalData }); 18 | 19 | expect(codeFromBundle.css).toBe(codeFromLess.css); 20 | expect(codeFromBundle.css).toMatchSnapshot("css"); 21 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 22 | expect(getErrors(stats)).toMatchSnapshot("errors"); 23 | }); 24 | 25 | it("should as function", async () => { 26 | const testId = "./additional-data.less"; 27 | const additionalData = (content, loaderContext) => { 28 | const { resourcePath, rootContext } = loaderContext; 29 | 30 | const relativePath = require("node:path").relative( 31 | rootContext, 32 | resourcePath, 33 | ); 34 | 35 | return ` 36 | /* RelativePath: ${relativePath}; */ 37 | 38 | @background: coral; 39 | ${content}; 40 | .custom-class {color: red}; 41 | `; 42 | }; 43 | const compiler = getCompiler(testId, { additionalData }); 44 | const stats = await compile(compiler); 45 | const codeFromBundle = getCodeFromBundle(stats, compiler); 46 | const codeFromLess = await getCodeFromLess(testId, { additionalData }); 47 | 48 | expect(codeFromBundle.css).toBe(codeFromLess.css); 49 | expect(codeFromBundle.css).toMatchSnapshot("css"); 50 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 51 | expect(getErrors(stats)).toMatchSnapshot("errors"); 52 | }); 53 | 54 | it("should work as async function", async () => { 55 | const testId = "./additional-data.less"; 56 | const additionalData = async (content, loaderContext) => { 57 | const { resourcePath, rootContext } = loaderContext; 58 | 59 | const relativePath = require("node:path").relative( 60 | rootContext, 61 | resourcePath, 62 | ); 63 | 64 | return ` 65 | /* RelativePath: ${relativePath}; */ 66 | 67 | @background: coral; 68 | ${content}; 69 | .custom-class {color: red}; 70 | `; 71 | }; 72 | const compiler = getCompiler(testId, { additionalData }); 73 | const stats = await compile(compiler); 74 | const codeFromBundle = getCodeFromBundle(stats, compiler); 75 | const codeFromLess = await getCodeFromLess(testId, { additionalData }); 76 | 77 | expect(codeFromBundle.css).toBe(codeFromLess.css); 78 | expect(codeFromBundle.css).toMatchSnapshot("css"); 79 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 80 | expect(getErrors(stats)).toMatchSnapshot("errors"); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/webpackImporter-options.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | compile, 3 | getCodeFromBundle, 4 | getCodeFromLess, 5 | getCompiler, 6 | getErrors, 7 | getWarnings, 8 | } from "./helpers"; 9 | 10 | describe('"webpackImporter" option', () => { 11 | it("should work when value is not specify", async () => { 12 | const testId = "./import-webpack.less"; 13 | const compiler = getCompiler(testId); 14 | const stats = await compile(compiler); 15 | const codeFromBundle = getCodeFromBundle(stats, compiler); 16 | const codeFromLess = await getCodeFromLess(testId); 17 | 18 | expect(codeFromBundle.css).toBe(codeFromLess.css); 19 | expect(codeFromBundle.css).toMatchSnapshot("css"); 20 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 21 | expect(getErrors(stats)).toMatchSnapshot("errors"); 22 | }); 23 | 24 | it('should work when value is "true"', async () => { 25 | const testId = "./import-webpack.less"; 26 | const compiler = getCompiler(testId, { 27 | webpackImporter: true, 28 | }); 29 | const stats = await compile(compiler); 30 | const codeFromBundle = getCodeFromBundle(stats, compiler); 31 | const codeFromLess = await getCodeFromLess(testId); 32 | 33 | expect(codeFromBundle.css).toBe(codeFromLess.css); 34 | expect(codeFromBundle.css).toMatchSnapshot("css"); 35 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 36 | expect(getErrors(stats)).toMatchSnapshot("errors"); 37 | }); 38 | 39 | it('should work when value is "only"', async () => { 40 | const testId = "./import-webpack.less"; 41 | const compiler = getCompiler(testId, { 42 | webpackImporter: "only", 43 | }); 44 | const stats = await compile(compiler); 45 | const codeFromBundle = getCodeFromBundle(stats, compiler); 46 | const codeFromLess = await getCodeFromLess(testId); 47 | 48 | expect(codeFromBundle.css).toBe(codeFromLess.css); 49 | expect(codeFromBundle.css).toMatchSnapshot("css"); 50 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 51 | expect(getErrors(stats)).toMatchSnapshot("errors"); 52 | }); 53 | 54 | it('should work when value is "false"', async () => { 55 | const testId = "./import.less"; 56 | const compiler = getCompiler(testId, { 57 | webpackImporter: false, 58 | }); 59 | const stats = await compile(compiler); 60 | const codeFromBundle = getCodeFromBundle(stats, compiler); 61 | const codeFromLess = await getCodeFromLess(testId); 62 | 63 | expect(codeFromBundle.css).toBe(codeFromLess.css); 64 | expect(codeFromBundle.css).toMatchSnapshot("css"); 65 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 66 | expect(getErrors(stats)).toMatchSnapshot("errors"); 67 | }); 68 | 69 | it('should throw an error on webpack import when value is "false"', async () => { 70 | const testId = "./import-webpack.less"; 71 | const compiler = getCompiler(testId, { 72 | webpackImporter: false, 73 | }); 74 | const stats = await compile(compiler); 75 | 76 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 77 | expect(getErrors(stats)).toMatchSnapshot("errors"); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "less-loader", 3 | "version": "12.3.0", 4 | "description": "A Less loader for webpack. Compiles Less to CSS.", 5 | "keywords": [ 6 | "webpack", 7 | "loader", 8 | "less", 9 | "lesscss", 10 | "less.js", 11 | "css", 12 | "preprocessor" 13 | ], 14 | "homepage": "https://github.com/webpack/less-loader", 15 | "bugs": "https://github.com/webpack/less-loader/issues", 16 | "repository": "webpack/less-loader", 17 | "funding": { 18 | "type": "opencollective", 19 | "url": "https://opencollective.com/webpack" 20 | }, 21 | "license": "MIT", 22 | "author": "Johannes Ewald @jhnns", 23 | "main": "dist/cjs.js", 24 | "files": [ 25 | "dist" 26 | ], 27 | "scripts": { 28 | "start": "npm run build -- -w", 29 | "clean": "del-cli dist", 30 | "prebuild": "npm run clean", 31 | "build": "cross-env NODE_ENV=production babel src -d dist --copy-files", 32 | "commitlint": "commitlint --from=main", 33 | "security": "npm audit --production", 34 | "lint:prettier": "prettier --cache --list-different .", 35 | "lint:js": "eslint --cache .", 36 | "lint:spelling": "cspell --cache --no-must-find-files --quiet \"**/*.*\"", 37 | "lint": "npm-run-all -l -p \"lint:**\"", 38 | "fix:js": "npm run lint:js -- --fix", 39 | "fix:prettier": "npm run lint:prettier -- --write", 40 | "fix": "npm-run-all -l fix:js fix:prettier", 41 | "test:only": "cross-env NODE_ENV=test jest", 42 | "test:watch": "npm run test:only -- --watch", 43 | "test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage", 44 | "pretest": "npm run lint", 45 | "test": "npm run test:coverage", 46 | "prepare": "husky && npm run build", 47 | "release": "standard-version" 48 | }, 49 | "devDependencies": { 50 | "@babel/cli": "^7.24.7", 51 | "@babel/core": "^7.24.7", 52 | "@babel/preset-env": "^7.24.7", 53 | "@commitlint/cli": "^18.6.1", 54 | "@commitlint/config-conventional": "^18.6.2", 55 | "@eslint/js": "^9.32.0", 56 | "@eslint/markdown": "^7.1.0", 57 | "@stylistic/eslint-plugin": "^5.2.2", 58 | "babel-jest": "^30.0.0", 59 | "cross-env": "^7.0.3", 60 | "cspell": "^8.10.0", 61 | "del": "^7.1.0", 62 | "del-cli": "^5.1.0", 63 | "eslint": "^9.32.0", 64 | "eslint-config-prettier": "^10.1.8", 65 | "eslint-config-webpack": "^4.5.1", 66 | "eslint-plugin-import": "^2.32.0", 67 | "eslint-plugin-jest": "^29.0.1", 68 | "eslint-plugin-n": "^17.21.1", 69 | "eslint-plugin-prettier": "^5.5.3", 70 | "eslint-plugin-unicorn": "^60.0.0", 71 | "globals": "^16.3.0", 72 | "husky": "^9.1.3", 73 | "jest": "^30.0.0", 74 | "less": "^4.2.0", 75 | "less-plugin-glob": "^3.0.0", 76 | "lint-staged": "^15.2.7", 77 | "memfs": "^4.9.3", 78 | "npm-run-all": "^4.1.5", 79 | "prettier": "^3.3.2", 80 | "standard-version": "^9.3.1", 81 | "strip-ansi": "^7.1.0", 82 | "typescript-eslint": "^8.38.0", 83 | "webpack": "^5.92.1" 84 | }, 85 | "peerDependencies": { 86 | "@rspack/core": "0.x || 1.x", 87 | "less": "^3.5.0 || ^4.0.0", 88 | "webpack": "^5.0.0" 89 | }, 90 | "peerDependenciesMeta": { 91 | "@rspack/core": { 92 | "optional": true 93 | }, 94 | "webpack": { 95 | "optional": true 96 | } 97 | }, 98 | "engines": { 99 | "node": ">= 18.12.0" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | import schema from "./options.json"; 4 | import { 5 | errorFactory, 6 | getLessImplementation, 7 | getLessOptions, 8 | isUnsupportedUrl, 9 | normalizeSourceMap, 10 | } from "./utils"; 11 | 12 | async function lessLoader(source) { 13 | const options = this.getOptions(schema); 14 | const callback = this.async(); 15 | let implementation; 16 | 17 | try { 18 | implementation = getLessImplementation(this, options.implementation); 19 | } catch (error) { 20 | callback(error); 21 | 22 | return; 23 | } 24 | 25 | if (!implementation) { 26 | callback( 27 | new Error( 28 | `The Less implementation "${options.implementation}" not found`, 29 | ), 30 | ); 31 | 32 | return; 33 | } 34 | 35 | const lessOptions = getLessOptions(this, options, implementation); 36 | const useSourceMap = 37 | typeof options.sourceMap === "boolean" ? options.sourceMap : this.sourceMap; 38 | 39 | if (useSourceMap) { 40 | lessOptions.sourceMap = { 41 | outputSourceFiles: true, 42 | }; 43 | } 44 | 45 | let data = source; 46 | 47 | if (typeof options.additionalData !== "undefined") { 48 | data = 49 | typeof options.additionalData === "function" 50 | ? `${await options.additionalData(data, this)}` 51 | : `${options.additionalData}\n${data}`; 52 | } 53 | 54 | const logger = this.getLogger("less-loader"); 55 | const loaderContext = this; 56 | const loggerListener = { 57 | error(message) { 58 | // TODO enable by default in the next major release 59 | if (options.lessLogAsWarnOrErr) { 60 | loaderContext.emitError(new Error(message)); 61 | } else { 62 | logger.error(message); 63 | } 64 | }, 65 | warn(message) { 66 | // TODO enable by default in the next major release 67 | if (options.lessLogAsWarnOrErr) { 68 | loaderContext.emitWarning(new Error(message)); 69 | } else { 70 | logger.warn(message); 71 | } 72 | }, 73 | info(message) { 74 | logger.log(message); 75 | }, 76 | debug(message) { 77 | logger.debug(message); 78 | }, 79 | }; 80 | 81 | implementation.logger.addListener(loggerListener); 82 | 83 | let result; 84 | 85 | try { 86 | result = await implementation.render(data, lessOptions); 87 | } catch (error) { 88 | if (error.filename) { 89 | // `less` returns forward slashes on windows when `webpack` resolver return an absolute windows path in `WebpackFileManager` 90 | // Ref: https://github.com/webpack/less-loader/issues/357 91 | this.addDependency(path.normalize(error.filename)); 92 | } 93 | 94 | callback(errorFactory(error)); 95 | 96 | return; 97 | } finally { 98 | // Fix memory leaks in `less` 99 | implementation.logger.removeListener(loggerListener); 100 | 101 | delete lessOptions.pluginManager.webpackLoaderContext; 102 | delete lessOptions.pluginManager; 103 | } 104 | 105 | const { css, imports } = result; 106 | 107 | for (const item of imports) { 108 | if (isUnsupportedUrl(item)) { 109 | continue; 110 | } 111 | 112 | // `less` return forward slashes on windows when `webpack` resolver return an absolute windows path in `WebpackFileManager` 113 | // Ref: https://github.com/webpack/less-loader/issues/357 114 | const normalizedItem = path.normalize(item); 115 | 116 | // Custom `importer` can return only `contents` so item will be relative 117 | if (path.isAbsolute(normalizedItem)) { 118 | this.addDependency(normalizedItem); 119 | } 120 | } 121 | 122 | let map = 123 | typeof result.map === "string" ? JSON.parse(result.map) : result.map; 124 | 125 | if (map && useSourceMap) { 126 | map = normalizeSourceMap(map, this.rootContext); 127 | } 128 | 129 | callback(null, css, map); 130 | } 131 | 132 | export default lessLoader; 133 | -------------------------------------------------------------------------------- /test/helpers/getCodeFromLess.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | import less from "less"; 5 | 6 | const pathMap = { 7 | "some/css.css": path.resolve( 8 | __dirname, 9 | "..", 10 | "fixtures", 11 | "node_modules", 12 | "some", 13 | "css.css", 14 | ), 15 | "~some/css.css": path.resolve( 16 | __dirname, 17 | "..", 18 | "fixtures", 19 | "node_modules", 20 | "some", 21 | "css.css", 22 | ), 23 | "some/module": path.resolve( 24 | __dirname, 25 | "..", 26 | "fixtures", 27 | "node_modules", 28 | "some", 29 | "module.less", 30 | ), 31 | "~some/module": path.resolve( 32 | __dirname, 33 | "..", 34 | "fixtures", 35 | "node_modules", 36 | "some", 37 | "module.less", 38 | ), 39 | "some/module.less": path.resolve( 40 | __dirname, 41 | "..", 42 | "fixtures", 43 | "node_modules", 44 | "some", 45 | "module.less", 46 | ), 47 | "module.less": path.resolve( 48 | __dirname, 49 | "..", 50 | "fixtures", 51 | "node_modules", 52 | "some", 53 | "module.less", 54 | ), 55 | "@scope/css.css": path.resolve( 56 | __dirname, 57 | "..", 58 | "fixtures", 59 | "node_modules", 60 | "@scope", 61 | "css.css", 62 | ), 63 | "@scope/module": path.resolve( 64 | __dirname, 65 | "..", 66 | "fixtures", 67 | "node_modules", 68 | "@scope", 69 | "module.less", 70 | ), 71 | "~fileAlias": path.resolve(__dirname, "..", "fixtures", "img.less"), 72 | fileAlias: path.resolve(__dirname, "..", "fixtures", "img.less"), 73 | "assets/basic.less": path.resolve(__dirname, "..", "fixtures", "basic.less"), 74 | "@{absolutePath}": path.resolve( 75 | __dirname, 76 | "..", 77 | "fixtures", 78 | "import-absolute-target.less", 79 | ), 80 | "package/style.less": path.resolve( 81 | __dirname, 82 | "..", 83 | "fixtures", 84 | "node_modules", 85 | "package", 86 | "style.less", 87 | ), 88 | "/styles/style.less": path.resolve(__dirname, "..", "fixtures", "basic.less"), 89 | "../../some.file": path.resolve( 90 | __dirname, 91 | "..", 92 | "fixtures", 93 | "folder", 94 | "some.file", 95 | ), 96 | "package-with-exports": path.resolve( 97 | __dirname, 98 | "..", 99 | "fixtures", 100 | "node_modules", 101 | "package-with-exports", 102 | "style.less", 103 | ), 104 | preferAlias: path.resolve( 105 | __dirname, 106 | "..", 107 | "fixtures", 108 | "prefer-relative", 109 | "index.less", 110 | ), 111 | "custom-main-files": path.resolve( 112 | __dirname, 113 | "..", 114 | "fixtures", 115 | "custom-main-files", 116 | "custom.less", 117 | ), 118 | "less-package-1/index.less": path.resolve( 119 | __dirname, 120 | "..", 121 | "fixtures", 122 | "node_modules", 123 | "less-package-2", 124 | "node_modules", 125 | "less-package-1", 126 | "index.less", 127 | ), 128 | "less-package-2": path.resolve( 129 | __dirname, 130 | "..", 131 | "fixtures", 132 | "node_modules", 133 | "less-package-2", 134 | "index.less", 135 | ), 136 | "./resolve-working-directory-a.less": path.resolve( 137 | __dirname, 138 | "..", 139 | "fixtures", 140 | "resolve-working-directory", 141 | "resolve-working-directory-a.less", 142 | ), 143 | "3rd/b.less": path.resolve(__dirname, "..", "fixtures", "3rd", "b.less"), 144 | }; 145 | 146 | class ResolvePlugin extends less.FileManager { 147 | supports(filename) { 148 | if (filename[0] === "/" || path.win32.isAbsolute(filename)) { 149 | return true; 150 | } 151 | 152 | if (this.isPathAbsolute(filename)) { 153 | return false; 154 | } 155 | 156 | return true; 157 | } 158 | 159 | supportsSync() { 160 | return false; 161 | } 162 | 163 | async loadFile(filename, ...args) { 164 | const result = 165 | pathMap[filename] || path.resolve(__dirname, "..", "fixtures", filename); 166 | 167 | return super.loadFile(result, ...args); 168 | } 169 | } 170 | 171 | class CustomImportPlugin { 172 | install(lessInstance, pluginManager) { 173 | pluginManager.addFileManager(new ResolvePlugin()); 174 | } 175 | } 176 | 177 | async function getCodeFromLess(testId, options = {}, context = {}) { 178 | let pathToFile; 179 | 180 | if (context.packageExportsCustomConditionTestVariant === 1) { 181 | pathToFile = path.resolve( 182 | __dirname, 183 | "..", 184 | "fixtures", 185 | "node_modules/package-with-exports-and-custom-condition/style-1.less", 186 | ); 187 | } else if (context.packageExportsCustomConditionTestVariant === 2) { 188 | pathToFile = path.resolve( 189 | __dirname, 190 | "..", 191 | "fixtures", 192 | "node_modules/package-with-exports-and-custom-condition/style-2.less", 193 | ); 194 | } else { 195 | pathToFile = path.resolve(__dirname, "..", "fixtures", testId); 196 | } 197 | 198 | const defaultOptions = { 199 | plugins: [], 200 | relativeUrls: true, 201 | filename: pathToFile, 202 | }; 203 | const lessOptions = options.lessOptions || {}; 204 | 205 | let data = await fs.promises.readFile(pathToFile); 206 | 207 | if (typeof options.additionalData !== "undefined") { 208 | data = 209 | typeof options.additionalData === "function" 210 | ? `${await options.additionalData(data, { 211 | rootContext: path.resolve(__dirname, "../fixtures"), 212 | resourcePath: pathToFile, 213 | })}` 214 | : `${options.additionalData}\n${data}`; 215 | } 216 | 217 | const mergedOptions = { 218 | ...defaultOptions, 219 | ...lessOptions, 220 | }; 221 | 222 | mergedOptions.plugins.unshift(new CustomImportPlugin()); 223 | 224 | return less.render(data.toString(), mergedOptions); 225 | } 226 | 227 | export default getCodeFromLess; 228 | -------------------------------------------------------------------------------- /test/__snapshots__/sourceMap-options.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`"sourceMap" options should generate source maps when the "devtool" value is "source-map": css 1`] = ` 4 | ".modules-dir-some-module, 5 | #it-works { 6 | color: hotpink; 7 | } 8 | #it-works { 9 | margin: 10px; 10 | } 11 | " 12 | `; 13 | 14 | exports[`"sourceMap" options should generate source maps when the "devtool" value is "source-map": errors 1`] = `[]`; 15 | 16 | exports[`"sourceMap" options should generate source maps when the "devtool" value is "source-map": source map 1`] = ` 17 | { 18 | "mappings": "AAAA;ACEA;EDDC,cAAA;;ACCD;EACE,YAAA", 19 | "names": [], 20 | "sourceRoot": "", 21 | "sources": [ 22 | "test/fixtures/node_modules/some/module.less", 23 | "test/fixtures/source-map.less", 24 | ], 25 | "sourcesContent": [ 26 | ".modules-dir-some-module { 27 | color: hotpink; 28 | } 29 | ", 30 | "@import "some/module"; 31 | 32 | #it-works:extend(.modules-dir-some-module) { 33 | margin: 10px; 34 | } 35 | ", 36 | ], 37 | "version": 3, 38 | } 39 | `; 40 | 41 | exports[`"sourceMap" options should generate source maps when the "devtool" value is "source-map": warnings 1`] = `[]`; 42 | 43 | exports[`"sourceMap" options should generate source maps when value has "false" value, but the "lessOptions.sourceMap.outputSourceFiles" is "true": css 1`] = ` 44 | ".modules-dir-some-module, 45 | #it-works { 46 | color: hotpink; 47 | } 48 | #it-works { 49 | margin: 10px; 50 | } 51 | " 52 | `; 53 | 54 | exports[`"sourceMap" options should generate source maps when value has "false" value, but the "lessOptions.sourceMap.outputSourceFiles" is "true": errors 1`] = `[]`; 55 | 56 | exports[`"sourceMap" options should generate source maps when value has "false" value, but the "lessOptions.sourceMap.outputSourceFiles" is "true": source map 1`] = ` 57 | { 58 | "mappings": "AAAA;ACEA;EDDC,cAAA;;ACCD;EACE,YAAA", 59 | "names": [], 60 | "sourceRoot": "", 61 | "sources": [ 62 | "test/fixtures/node_modules/some/module.less", 63 | "test/fixtures/source-map.less", 64 | ], 65 | "sourcesContent": [ 66 | ".modules-dir-some-module { 67 | color: hotpink; 68 | } 69 | ", 70 | "@import "some/module"; 71 | 72 | #it-works:extend(.modules-dir-some-module) { 73 | margin: 10px; 74 | } 75 | ", 76 | ], 77 | "version": 3, 78 | } 79 | `; 80 | 81 | exports[`"sourceMap" options should generate source maps when value has "false" value, but the "lessOptions.sourceMap.outputSourceFiles" is "true": warnings 1`] = `[]`; 82 | 83 | exports[`"sourceMap" options should generate source maps when value is "true" and the "devtool" value is "false": css 1`] = ` 84 | ".modules-dir-some-module, 85 | #it-works { 86 | color: hotpink; 87 | } 88 | #it-works { 89 | margin: 10px; 90 | } 91 | " 92 | `; 93 | 94 | exports[`"sourceMap" options should generate source maps when value is "true" and the "devtool" value is "false": errors 1`] = `[]`; 95 | 96 | exports[`"sourceMap" options should generate source maps when value is "true" and the "devtool" value is "false": source map 1`] = ` 97 | { 98 | "mappings": "AAAA;ACEA;EDDC,cAAA;;ACCD;EACE,YAAA", 99 | "names": [], 100 | "sourceRoot": "", 101 | "sources": [ 102 | "test/fixtures/node_modules/some/module.less", 103 | "test/fixtures/source-map.less", 104 | ], 105 | "sourcesContent": [ 106 | ".modules-dir-some-module { 107 | color: hotpink; 108 | } 109 | ", 110 | "@import "some/module"; 111 | 112 | #it-works:extend(.modules-dir-some-module) { 113 | margin: 10px; 114 | } 115 | ", 116 | ], 117 | "version": 3, 118 | } 119 | `; 120 | 121 | exports[`"sourceMap" options should generate source maps when value is "true" and the "devtool" value is "false": warnings 1`] = `[]`; 122 | 123 | exports[`"sourceMap" options should generate source maps when value is "true": css 1`] = ` 124 | ".modules-dir-some-module, 125 | #it-works { 126 | color: hotpink; 127 | } 128 | #it-works { 129 | margin: 10px; 130 | } 131 | " 132 | `; 133 | 134 | exports[`"sourceMap" options should generate source maps when value is "true": errors 1`] = `[]`; 135 | 136 | exports[`"sourceMap" options should generate source maps when value is "true": source map 1`] = ` 137 | { 138 | "mappings": "AAAA;ACEA;EDDC,cAAA;;ACCD;EACE,YAAA", 139 | "names": [], 140 | "sourceRoot": "", 141 | "sources": [ 142 | "test/fixtures/node_modules/some/module.less", 143 | "test/fixtures/source-map.less", 144 | ], 145 | "sourcesContent": [ 146 | ".modules-dir-some-module { 147 | color: hotpink; 148 | } 149 | ", 150 | "@import "some/module"; 151 | 152 | #it-works:extend(.modules-dir-some-module) { 153 | margin: 10px; 154 | } 155 | ", 156 | ], 157 | "version": 3, 158 | } 159 | `; 160 | 161 | exports[`"sourceMap" options should generate source maps when value is "true": warnings 1`] = `[]`; 162 | 163 | exports[`"sourceMap" options should not generate source maps when the "devtool" value is "false": css 1`] = ` 164 | ".modules-dir-some-module, 165 | #it-works { 166 | color: hotpink; 167 | } 168 | #it-works { 169 | margin: 10px; 170 | } 171 | " 172 | `; 173 | 174 | exports[`"sourceMap" options should not generate source maps when the "devtool" value is "false": errors 1`] = `[]`; 175 | 176 | exports[`"sourceMap" options should not generate source maps when the "devtool" value is "false": warnings 1`] = `[]`; 177 | 178 | exports[`"sourceMap" options should not generate source maps when value is "false" and the "devtool" value is "source-map": css 1`] = ` 179 | ".modules-dir-some-module, 180 | #it-works { 181 | color: hotpink; 182 | } 183 | #it-works { 184 | margin: 10px; 185 | } 186 | " 187 | `; 188 | 189 | exports[`"sourceMap" options should not generate source maps when value is "false" and the "devtool" value is "source-map": errors 1`] = `[]`; 190 | 191 | exports[`"sourceMap" options should not generate source maps when value is "false" and the "devtool" value is "source-map": warnings 1`] = `[]`; 192 | 193 | exports[`"sourceMap" options should not generate source maps when value is "false": css 1`] = ` 194 | ".modules-dir-some-module, 195 | #it-works { 196 | color: hotpink; 197 | } 198 | #it-works { 199 | margin: 10px; 200 | } 201 | " 202 | `; 203 | 204 | exports[`"sourceMap" options should not generate source maps when value is "false": errors 1`] = `[]`; 205 | 206 | exports[`"sourceMap" options should not generate source maps when value is "false": warnings 1`] = `[]`; 207 | 208 | exports[`"sourceMap" options should work and generate custom source maps: css 1`] = ` 209 | ".modules-dir-some-module, 210 | #it-works { 211 | color: hotpink; 212 | } 213 | #it-works { 214 | margin: 10px; 215 | } 216 | /*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9zb21lL21vZHVsZS5sZXNzIiwic291cmNlLW1hcC5sZXNzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBO0FDRUE7RUREQyxjQUFBOztBQ0NEO0VBQ0UsWUFBQSIsInNvdXJjZXNDb250ZW50IjpbIi5tb2R1bGVzLWRpci1zb21lLW1vZHVsZSB7XG5cdGNvbG9yOiBob3RwaW5rO1xufVxuIiwiQGltcG9ydCBcInNvbWUvbW9kdWxlXCI7XG5cbiNpdC13b3JrczpleHRlbmQoLm1vZHVsZXMtZGlyLXNvbWUtbW9kdWxlKSB7XG4gIG1hcmdpbjogMTBweDtcbn1cbiJdfQ== */" 217 | `; 218 | 219 | exports[`"sourceMap" options should work and generate custom source maps: errors 1`] = `[]`; 220 | 221 | exports[`"sourceMap" options should work and generate custom source maps: source map 1`] = ` 222 | { 223 | "mappings": "AAAA;ACEA;EDDC,cAAA;;ACCD;EACE,YAAA", 224 | "names": [], 225 | "sources": [ 226 | "node_modules/some/module.less", 227 | "source-map.less", 228 | ], 229 | "sourcesContent": [ 230 | ".modules-dir-some-module { 231 | color: hotpink; 232 | } 233 | ", 234 | "@import "some/module"; 235 | 236 | #it-works:extend(.modules-dir-some-module) { 237 | margin: 10px; 238 | } 239 | ", 240 | ], 241 | "version": 3, 242 | } 243 | `; 244 | 245 | exports[`"sourceMap" options should work and generate custom source maps: warnings 1`] = `[]`; 246 | -------------------------------------------------------------------------------- /test/sourceMap-options.test.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | import { 5 | compile, 6 | getCodeFromBundle, 7 | getCodeFromLess, 8 | getCompiler, 9 | getErrors, 10 | getWarnings, 11 | } from "./helpers"; 12 | 13 | describe('"sourceMap" options', () => { 14 | it('should generate source maps when value is "true"', async () => { 15 | const testId = "./source-map.less"; 16 | const compiler = getCompiler(testId, { 17 | sourceMap: true, 18 | }); 19 | const stats = await compile(compiler); 20 | const codeFromBundle = getCodeFromBundle(stats, compiler); 21 | const codeFromLess = await getCodeFromLess(testId); 22 | const { css, map } = codeFromBundle; 23 | 24 | map.sourceRoot = ""; 25 | map.sources = map.sources.map((source) => { 26 | expect(path.isAbsolute(source)).toBe(true); 27 | expect(source).toBe(path.normalize(source)); 28 | expect(fs.existsSync(path.resolve(map.sourceRoot, source))).toBe(true); 29 | 30 | return path 31 | .relative(path.resolve(__dirname, ".."), source) 32 | .replaceAll("\\", "/"); 33 | }); 34 | 35 | expect(css).toBe(codeFromLess.css); 36 | expect(css).toMatchSnapshot("css"); 37 | expect(map).toMatchSnapshot("source map"); 38 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 39 | expect(getErrors(stats)).toMatchSnapshot("errors"); 40 | }); 41 | 42 | it('should generate source maps when the "devtool" value is "source-map"', async () => { 43 | const testId = "./source-map.less"; 44 | const compiler = getCompiler( 45 | testId, 46 | {}, 47 | { 48 | devtool: "source-map", 49 | }, 50 | ); 51 | const stats = await compile(compiler); 52 | const codeFromBundle = getCodeFromBundle(stats, compiler); 53 | const codeFromLess = await getCodeFromLess(testId); 54 | const { css, map } = codeFromBundle; 55 | 56 | map.sourceRoot = ""; 57 | map.sources = map.sources.map((source) => { 58 | expect(path.isAbsolute(source)).toBe(true); 59 | expect(source).toBe(path.normalize(source)); 60 | expect(fs.existsSync(path.resolve(map.sourceRoot, source))).toBe(true); 61 | 62 | return path 63 | .relative(path.resolve(__dirname, ".."), source) 64 | .replaceAll("\\", "/"); 65 | }); 66 | 67 | expect(css).toBe(codeFromLess.css); 68 | expect(css).toMatchSnapshot("css"); 69 | expect(map).toMatchSnapshot("source map"); 70 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 71 | expect(getErrors(stats)).toMatchSnapshot("errors"); 72 | }); 73 | 74 | it('should generate source maps when value is "true" and the "devtool" value is "false"', async () => { 75 | const testId = "./source-map.less"; 76 | const compiler = getCompiler( 77 | testId, 78 | { 79 | sourceMap: true, 80 | }, 81 | { 82 | devtool: false, 83 | }, 84 | ); 85 | const stats = await compile(compiler); 86 | const codeFromBundle = getCodeFromBundle(stats, compiler); 87 | const codeFromLess = await getCodeFromLess(testId); 88 | const { css, map } = codeFromBundle; 89 | 90 | map.sourceRoot = ""; 91 | map.sources = map.sources.map((source) => { 92 | expect(path.isAbsolute(source)).toBe(true); 93 | expect(source).toBe(path.normalize(source)); 94 | expect(fs.existsSync(path.resolve(map.sourceRoot, source))).toBe(true); 95 | 96 | return path 97 | .relative(path.resolve(__dirname, ".."), source) 98 | .replaceAll("\\", "/"); 99 | }); 100 | 101 | expect(css).toBe(codeFromLess.css); 102 | expect(css).toMatchSnapshot("css"); 103 | expect(map).toMatchSnapshot("source map"); 104 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 105 | expect(getErrors(stats)).toMatchSnapshot("errors"); 106 | }); 107 | 108 | it('should generate source maps when value has "false" value, but the "lessOptions.sourceMap.outputSourceFiles" is "true"', async () => { 109 | const testId = "./source-map.less"; 110 | const compiler = getCompiler(testId, { 111 | sourceMap: false, 112 | lessOptions: { 113 | sourceMap: { outputSourceFiles: true }, 114 | }, 115 | }); 116 | const stats = await compile(compiler); 117 | const codeFromBundle = getCodeFromBundle(stats, compiler); 118 | const codeFromLess = await getCodeFromLess(testId); 119 | const { css, map } = codeFromBundle; 120 | 121 | map.sourceRoot = ""; 122 | map.sources = map.sources.map((source) => { 123 | expect(path.isAbsolute(source)).toBe(true); 124 | expect(fs.existsSync(path.resolve(map.sourceRoot, source))).toBe(true); 125 | 126 | return path 127 | .relative(path.resolve(__dirname, ".."), source) 128 | .replaceAll("\\", "/"); 129 | }); 130 | 131 | expect(css).toBe(codeFromLess.css); 132 | expect(css).toMatchSnapshot("css"); 133 | expect(map).toMatchSnapshot("source map"); 134 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 135 | expect(getErrors(stats)).toMatchSnapshot("errors"); 136 | }); 137 | 138 | it('should not generate source maps when value is "false"', async () => { 139 | const testId = "./source-map.less"; 140 | const compiler = getCompiler(testId, { 141 | sourceMap: false, 142 | }); 143 | const stats = await compile(compiler); 144 | const codeFromBundle = getCodeFromBundle(stats, compiler); 145 | const codeFromLess = await getCodeFromLess(testId); 146 | const { css, map } = codeFromBundle; 147 | 148 | expect(css).toBe(codeFromLess.css); 149 | expect(css).toMatchSnapshot("css"); 150 | expect(map).toBeUndefined(); 151 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 152 | expect(getErrors(stats)).toMatchSnapshot("errors"); 153 | }); 154 | 155 | it('should not generate source maps when the "devtool" value is "false"', async () => { 156 | const testId = "./source-map.less"; 157 | const compiler = getCompiler( 158 | testId, 159 | {}, 160 | { 161 | devtool: false, 162 | }, 163 | ); 164 | const stats = await compile(compiler); 165 | const codeFromBundle = getCodeFromBundle(stats, compiler); 166 | const codeFromLess = await getCodeFromLess(testId); 167 | const { css, map } = codeFromBundle; 168 | 169 | expect(css).toBe(codeFromLess.css); 170 | expect(css).toMatchSnapshot("css"); 171 | expect(map).toBeUndefined(); 172 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 173 | expect(getErrors(stats)).toMatchSnapshot("errors"); 174 | }); 175 | 176 | it('should not generate source maps when value is "false" and the "devtool" value is "source-map"', async () => { 177 | const testId = "./source-map.less"; 178 | const compiler = getCompiler( 179 | testId, 180 | { 181 | sourceMap: false, 182 | }, 183 | { 184 | devtool: "source-map", 185 | }, 186 | ); 187 | const stats = await compile(compiler); 188 | const codeFromBundle = getCodeFromBundle(stats, compiler); 189 | const codeFromLess = await getCodeFromLess(testId); 190 | const { css, map } = codeFromBundle; 191 | 192 | expect(css).toBe(codeFromLess.css); 193 | expect(css).toMatchSnapshot("css"); 194 | expect(map).toBeUndefined(); 195 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 196 | expect(getErrors(stats)).toMatchSnapshot("errors"); 197 | }); 198 | 199 | it("should work and generate custom source maps", async () => { 200 | const testId = "./source-map.less"; 201 | const lessOptions = { 202 | sourceMap: { 203 | sourceMapFileInline: true, 204 | // cspell:disable-next-line 205 | sourceMapBasepath: path.resolve(__dirname, "fixtures"), 206 | outputSourceFiles: true, 207 | }, 208 | }; 209 | const options = { lessOptions }; 210 | const compiler = getCompiler(testId, options); 211 | const stats = await compile(compiler); 212 | const codeFromBundle = getCodeFromBundle(stats, compiler); 213 | const codeFromLess = await getCodeFromLess(testId, options); 214 | const { css, map } = codeFromBundle; 215 | 216 | expect(css).toBe(codeFromLess.css); 217 | expect(css).toMatchSnapshot("css"); 218 | expect(map).toMatchSnapshot("source map"); 219 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 220 | expect(getErrors(stats)).toMatchSnapshot("errors"); 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | 3 | const trailingSlash = /[/\\]$/; 4 | 5 | // This somewhat changed in Less 3.x. Now the file name comes without the 6 | // automatically added extension whereas the extension is passed in as `options.ext`. 7 | // So, if the file name matches this regexp, we simply ignore the proposed extension. 8 | const IS_SPECIAL_MODULE_IMPORT = /^~[^/]+$/; 9 | 10 | // `[drive_letter]:\` + `\\[server]\[share_name]\` 11 | const IS_NATIVE_WIN32_PATH = /^[a-z]:[/\\]|^\\\\/i; 12 | 13 | // Examples: 14 | // - ~package 15 | // - ~package/ 16 | // - ~@org 17 | // - ~@org/ 18 | // - ~@org/package 19 | // - ~@org/package/ 20 | const IS_MODULE_IMPORT = 21 | /^~([^/]+|[^/]+\/|@[^/]+[/][^/]+|@[^/]+\/?|@[^/]+[/][^/]+\/)$/; 22 | const MODULE_REQUEST_REGEX = /^[^?]*~/; 23 | 24 | /** 25 | * Creates a Less plugin that uses webpack's resolving engine that is provided by the loaderContext. 26 | * 27 | * @param {LoaderContext} loaderContext 28 | * @param {object} implementation 29 | * @returns {LessPlugin} 30 | */ 31 | function createWebpackLessPlugin(loaderContext, implementation) { 32 | const lessOptions = loaderContext.getOptions(); 33 | const resolve = loaderContext.getResolve({ 34 | dependencyType: "less", 35 | conditionNames: ["less", "style", "..."], 36 | mainFields: ["less", "style", "main", "..."], 37 | mainFiles: ["index", "..."], 38 | extensions: [".less", ".css"], 39 | preferRelative: true, 40 | }); 41 | 42 | class WebpackFileManager extends implementation.FileManager { 43 | supports(filename) { 44 | if (filename[0] === "/" || IS_NATIVE_WIN32_PATH.test(filename)) { 45 | return true; 46 | } 47 | 48 | if (this.isPathAbsolute(filename)) { 49 | return false; 50 | } 51 | 52 | return true; 53 | } 54 | 55 | // Sync resolving is used at least by the `data-uri` function. 56 | // This file manager doesn't know how to do it, so let's delegate it 57 | // to the default file manager of Less. 58 | // We could probably use loaderContext.resolveSync, but it's deprecated, 59 | // see https://webpack.js.org/api/loaders/#this-resolvesync 60 | supportsSync() { 61 | return false; 62 | } 63 | 64 | async resolveFilename(filename, currentDirectory) { 65 | // Less is giving us trailing slashes, but the context should have no trailing slash 66 | const context = currentDirectory.replace(trailingSlash, ""); 67 | 68 | let request = filename; 69 | 70 | // A `~` makes the url an module 71 | if (MODULE_REQUEST_REGEX.test(filename)) { 72 | request = request.replace(MODULE_REQUEST_REGEX, ""); 73 | } 74 | 75 | if (IS_MODULE_IMPORT.test(filename)) { 76 | request = request[request.length - 1] === "/" ? request : `${request}/`; 77 | } 78 | 79 | return this.resolveRequests(context, [...new Set([request, filename])]); 80 | } 81 | 82 | async resolveRequests(context, possibleRequests) { 83 | if (possibleRequests.length === 0) { 84 | throw new Error("No possible requests to resolve"); 85 | } 86 | 87 | let result; 88 | 89 | try { 90 | result = await resolve(context, possibleRequests[0]); 91 | } catch (error) { 92 | const [, ...tailPossibleRequests] = possibleRequests; 93 | 94 | if (tailPossibleRequests.length === 0) { 95 | throw error; 96 | } 97 | 98 | result = await this.resolveRequests(context, tailPossibleRequests); 99 | } 100 | 101 | return result; 102 | } 103 | 104 | async loadFile(filename, ...args) { 105 | let result; 106 | 107 | try { 108 | if ( 109 | IS_SPECIAL_MODULE_IMPORT.test(filename) || 110 | lessOptions.webpackImporter === "only" 111 | ) { 112 | const error = new Error("Next"); 113 | 114 | error.type = "Next"; 115 | 116 | throw error; 117 | } 118 | 119 | result = await super.loadFile(filename, ...args); 120 | } catch (error) { 121 | if (error.type !== "File" && error.type !== "Next") { 122 | throw error; 123 | } 124 | 125 | try { 126 | result = await this.resolveFilename(filename, ...args); 127 | } catch (err) { 128 | error.message = 129 | `Less resolver error:\n${error.message}\n\n` + 130 | `Webpack resolver error details:\n${err.details}\n\n` + 131 | `Webpack resolver error missing:\n${err.missing}\n\n`; 132 | 133 | throw error; 134 | } 135 | 136 | loaderContext.addDependency(result); 137 | 138 | return super.loadFile(result, ...args); 139 | } 140 | 141 | const absoluteFilename = path.isAbsolute(result.filename) 142 | ? result.filename 143 | : path.resolve(".", result.filename); 144 | 145 | loaderContext.addDependency(path.normalize(absoluteFilename)); 146 | 147 | return result; 148 | } 149 | } 150 | 151 | return { 152 | install(lessInstance, pluginManager) { 153 | pluginManager.addFileManager(new WebpackFileManager()); 154 | }, 155 | minVersion: [3, 0, 0], 156 | }; 157 | } 158 | 159 | /** 160 | * Get the `less` options from the loader context and normalizes its values 161 | * 162 | * @param {object} loaderContext 163 | * @param {object} loaderOptions 164 | * @param {object} implementation 165 | * @returns {Object} 166 | */ 167 | function getLessOptions(loaderContext, loaderOptions, implementation) { 168 | const options = 169 | typeof loaderOptions.lessOptions === "function" 170 | ? loaderOptions.lessOptions(loaderContext) || {} 171 | : loaderOptions.lessOptions || {}; 172 | 173 | const lessOptions = { 174 | plugins: [], 175 | relativeUrls: true, 176 | // We need to set the filename because otherwise our WebpackFileManager will receive an undefined path for the entry 177 | filename: loaderContext.resourcePath, 178 | ...options, 179 | }; 180 | 181 | const plugins = [...lessOptions.plugins]; 182 | const shouldUseWebpackImporter = 183 | typeof loaderOptions.webpackImporter === "boolean" || 184 | loaderOptions.webpackImporter === "only" 185 | ? loaderOptions.webpackImporter 186 | : true; 187 | 188 | if (shouldUseWebpackImporter) { 189 | plugins.unshift(createWebpackLessPlugin(loaderContext, implementation)); 190 | } 191 | 192 | plugins.unshift({ 193 | install(lessProcessor, pluginManager) { 194 | pluginManager.webpackLoaderContext = loaderContext; 195 | 196 | lessOptions.pluginManager = pluginManager; 197 | }, 198 | }); 199 | 200 | lessOptions.plugins = plugins; 201 | 202 | return lessOptions; 203 | } 204 | 205 | function isUnsupportedUrl(url) { 206 | // Is Windows path 207 | if (IS_NATIVE_WIN32_PATH.test(url)) { 208 | return false; 209 | } 210 | 211 | // Scheme: https://tools.ietf.org/html/rfc3986#section-3.1 212 | // Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3 213 | return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url); 214 | } 215 | 216 | function normalizeSourceMap(map) { 217 | const newMap = map; 218 | 219 | // map.file is an optional property that provides the output filename. 220 | // Since we don't know the final filename in the webpack build chain yet, it makes no sense to have it. 221 | 222 | delete newMap.file; 223 | 224 | newMap.sourceRoot = ""; 225 | 226 | // `less` returns POSIX paths, that's why we need to transform them back to native paths. 227 | 228 | newMap.sources = newMap.sources.map((source) => path.normalize(source)); 229 | 230 | return newMap; 231 | } 232 | 233 | function getLessImplementation(loaderContext, implementation) { 234 | let resolvedImplementation = implementation; 235 | 236 | if (!implementation || typeof implementation === "string") { 237 | const lessImplPkg = implementation || "less"; 238 | 239 | resolvedImplementation = require(lessImplPkg); 240 | } 241 | 242 | return resolvedImplementation; 243 | } 244 | 245 | function getFileExcerptIfPossible(error) { 246 | if (typeof error.extract === "undefined") { 247 | return []; 248 | } 249 | 250 | const excerpt = error.extract.slice(0, 2); 251 | const column = Math.max(error.column - 1, 0); 252 | 253 | if (typeof excerpt[0] === "undefined") { 254 | excerpt.shift(); 255 | } 256 | 257 | excerpt.push(`${" ".repeat(column)}^`); 258 | 259 | return excerpt; 260 | } 261 | 262 | function errorFactory(error) { 263 | const message = [ 264 | "\n", 265 | ...getFileExcerptIfPossible(error), 266 | error.message.charAt(0).toUpperCase() + error.message.slice(1), 267 | error.filename 268 | ? ` Error in ${path.normalize(error.filename)} (line ${ 269 | error.line 270 | }, column ${error.column})` 271 | : "", 272 | ].join("\n"); 273 | 274 | const obj = new Error(message, { cause: error }); 275 | 276 | obj.stack = null; 277 | 278 | return obj; 279 | } 280 | 281 | export { 282 | errorFactory, 283 | getLessImplementation, 284 | getLessOptions, 285 | isUnsupportedUrl, 286 | normalizeSourceMap, 287 | }; 288 | -------------------------------------------------------------------------------- /test/__snapshots__/validate-options.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`validate options should throw an error on the "additionalData" option with "/test/" value 1`] = ` 4 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 5 | - options.additionalData should be one of these: 6 | string | function 7 | -> Prepends/Appends \`Less\` code to the actual entry file. 8 | -> Read more at https://github.com/webpack/less-loader#additionalData 9 | Details: 10 | * options.additionalData should be a string. 11 | * options.additionalData should be an instance of function." 12 | `; 13 | 14 | exports[`validate options should throw an error on the "additionalData" option with "[]" value 1`] = ` 15 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 16 | - options.additionalData should be one of these: 17 | string | function 18 | -> Prepends/Appends \`Less\` code to the actual entry file. 19 | -> Read more at https://github.com/webpack/less-loader#additionalData 20 | Details: 21 | * options.additionalData should be a string. 22 | * options.additionalData should be an instance of function." 23 | `; 24 | 25 | exports[`validate options should throw an error on the "additionalData" option with "{}" value 1`] = ` 26 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 27 | - options.additionalData should be one of these: 28 | string | function 29 | -> Prepends/Appends \`Less\` code to the actual entry file. 30 | -> Read more at https://github.com/webpack/less-loader#additionalData 31 | Details: 32 | * options.additionalData should be a string. 33 | * options.additionalData should be an instance of function." 34 | `; 35 | 36 | exports[`validate options should throw an error on the "additionalData" option with "1" value 1`] = ` 37 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 38 | - options.additionalData should be one of these: 39 | string | function 40 | -> Prepends/Appends \`Less\` code to the actual entry file. 41 | -> Read more at https://github.com/webpack/less-loader#additionalData 42 | Details: 43 | * options.additionalData should be a string. 44 | * options.additionalData should be an instance of function." 45 | `; 46 | 47 | exports[`validate options should throw an error on the "additionalData" option with "false" value 1`] = ` 48 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 49 | - options.additionalData should be one of these: 50 | string | function 51 | -> Prepends/Appends \`Less\` code to the actual entry file. 52 | -> Read more at https://github.com/webpack/less-loader#additionalData 53 | Details: 54 | * options.additionalData should be a string. 55 | * options.additionalData should be an instance of function." 56 | `; 57 | 58 | exports[`validate options should throw an error on the "additionalData" option with "true" value 1`] = ` 59 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 60 | - options.additionalData should be one of these: 61 | string | function 62 | -> Prepends/Appends \`Less\` code to the actual entry file. 63 | -> Read more at https://github.com/webpack/less-loader#additionalData 64 | Details: 65 | * options.additionalData should be a string. 66 | * options.additionalData should be an instance of function." 67 | `; 68 | 69 | exports[`validate options should throw an error on the "implementation" option with "() => {}" value 1`] = ` 70 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 71 | - options.implementation should be one of these: 72 | string | object { … } 73 | -> The implementation of the \`Less\` to be used. 74 | -> Read more at https://github.com/webpack/less-loader#implementation 75 | Details: 76 | * options.implementation should be a string. 77 | * options.implementation should be an object: 78 | object { … }" 79 | `; 80 | 81 | exports[`validate options should throw an error on the "implementation" option with "[]" value 1`] = ` 82 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 83 | - options.implementation should be one of these: 84 | string | object { … } 85 | -> The implementation of the \`Less\` to be used. 86 | -> Read more at https://github.com/webpack/less-loader#implementation 87 | Details: 88 | * options.implementation should be a string. 89 | * options.implementation should be an object: 90 | object { … }" 91 | `; 92 | 93 | exports[`validate options should throw an error on the "implementation" option with "false" value 1`] = ` 94 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 95 | - options.implementation should be one of these: 96 | string | object { … } 97 | -> The implementation of the \`Less\` to be used. 98 | -> Read more at https://github.com/webpack/less-loader#implementation 99 | Details: 100 | * options.implementation should be a string. 101 | * options.implementation should be an object: 102 | object { … }" 103 | `; 104 | 105 | exports[`validate options should throw an error on the "implementation" option with "true" value 1`] = ` 106 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 107 | - options.implementation should be one of these: 108 | string | object { … } 109 | -> The implementation of the \`Less\` to be used. 110 | -> Read more at https://github.com/webpack/less-loader#implementation 111 | Details: 112 | * options.implementation should be a string. 113 | * options.implementation should be an object: 114 | object { … }" 115 | `; 116 | 117 | exports[`validate options should throw an error on the "lessLogAsWarnOrErr" option with "string" value 1`] = ` 118 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 119 | - options.lessLogAsWarnOrErr should be a boolean. 120 | -> Less warnings and errors will be webpack warnings or errors. 121 | -> Read more at https://github.com/webpack/less-loader#lesslogaswarnorerr" 122 | `; 123 | 124 | exports[`validate options should throw an error on the "lessOptions" option with "[]" value 1`] = ` 125 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 126 | - options.lessOptions should be one of these: 127 | object { … } | function 128 | -> Options to pass through to \`Less\`. 129 | -> Read more at https://github.com/webpack/less-loader#lessoptions 130 | Details: 131 | * options.lessOptions should be an object: 132 | object { … } 133 | * options.lessOptions should be an instance of function." 134 | `; 135 | 136 | exports[`validate options should throw an error on the "lessOptions" option with "1" value 1`] = ` 137 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 138 | - options.lessOptions should be one of these: 139 | object { … } | function 140 | -> Options to pass through to \`Less\`. 141 | -> Read more at https://github.com/webpack/less-loader#lessoptions 142 | Details: 143 | * options.lessOptions should be an object: 144 | object { … } 145 | * options.lessOptions should be an instance of function." 146 | `; 147 | 148 | exports[`validate options should throw an error on the "lessOptions" option with "false" value 1`] = ` 149 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 150 | - options.lessOptions should be one of these: 151 | object { … } | function 152 | -> Options to pass through to \`Less\`. 153 | -> Read more at https://github.com/webpack/less-loader#lessoptions 154 | Details: 155 | * options.lessOptions should be an object: 156 | object { … } 157 | * options.lessOptions should be an instance of function." 158 | `; 159 | 160 | exports[`validate options should throw an error on the "lessOptions" option with "test" value 1`] = ` 161 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 162 | - options.lessOptions should be one of these: 163 | object { … } | function 164 | -> Options to pass through to \`Less\`. 165 | -> Read more at https://github.com/webpack/less-loader#lessoptions 166 | Details: 167 | * options.lessOptions should be an object: 168 | object { … } 169 | * options.lessOptions should be an instance of function." 170 | `; 171 | 172 | exports[`validate options should throw an error on the "lessOptions" option with "true" value 1`] = ` 173 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 174 | - options.lessOptions should be one of these: 175 | object { … } | function 176 | -> Options to pass through to \`Less\`. 177 | -> Read more at https://github.com/webpack/less-loader#lessoptions 178 | Details: 179 | * options.lessOptions should be an object: 180 | object { … } 181 | * options.lessOptions should be an instance of function." 182 | `; 183 | 184 | exports[`validate options should throw an error on the "sourceMap" option with "string" value 1`] = ` 185 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 186 | - options.sourceMap should be a boolean. 187 | -> Enables/Disables generation of source maps. 188 | -> Read more at https://github.com/webpack/less-loader#sourcemap" 189 | `; 190 | 191 | exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = ` 192 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 193 | - options has an unknown property 'unknown'. These properties are valid: 194 | object { lessOptions?, additionalData?, sourceMap?, webpackImporter?, implementation?, lessLogAsWarnOrErr? }" 195 | `; 196 | 197 | exports[`validate options should throw an error on the "unknown" option with "[]" value 1`] = ` 198 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 199 | - options has an unknown property 'unknown'. These properties are valid: 200 | object { lessOptions?, additionalData?, sourceMap?, webpackImporter?, implementation?, lessLogAsWarnOrErr? }" 201 | `; 202 | 203 | exports[`validate options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = ` 204 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 205 | - options has an unknown property 'unknown'. These properties are valid: 206 | object { lessOptions?, additionalData?, sourceMap?, webpackImporter?, implementation?, lessLogAsWarnOrErr? }" 207 | `; 208 | 209 | exports[`validate options should throw an error on the "unknown" option with "{}" value 1`] = ` 210 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 211 | - options has an unknown property 'unknown'. These properties are valid: 212 | object { lessOptions?, additionalData?, sourceMap?, webpackImporter?, implementation?, lessLogAsWarnOrErr? }" 213 | `; 214 | 215 | exports[`validate options should throw an error on the "unknown" option with "1" value 1`] = ` 216 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 217 | - options has an unknown property 'unknown'. These properties are valid: 218 | object { lessOptions?, additionalData?, sourceMap?, webpackImporter?, implementation?, lessLogAsWarnOrErr? }" 219 | `; 220 | 221 | exports[`validate options should throw an error on the "unknown" option with "false" value 1`] = ` 222 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 223 | - options has an unknown property 'unknown'. These properties are valid: 224 | object { lessOptions?, additionalData?, sourceMap?, webpackImporter?, implementation?, lessLogAsWarnOrErr? }" 225 | `; 226 | 227 | exports[`validate options should throw an error on the "unknown" option with "test" value 1`] = ` 228 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 229 | - options has an unknown property 'unknown'. These properties are valid: 230 | object { lessOptions?, additionalData?, sourceMap?, webpackImporter?, implementation?, lessLogAsWarnOrErr? }" 231 | `; 232 | 233 | exports[`validate options should throw an error on the "unknown" option with "true" value 1`] = ` 234 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 235 | - options has an unknown property 'unknown'. These properties are valid: 236 | object { lessOptions?, additionalData?, sourceMap?, webpackImporter?, implementation?, lessLogAsWarnOrErr? }" 237 | `; 238 | 239 | exports[`validate options should throw an error on the "webpackImporter" option with "string" value 1`] = ` 240 | "Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. 241 | - options.webpackImporter should be one of these: 242 | boolean | "only" 243 | -> Enables/Disables default \`webpack\` importer. 244 | -> Read more at https://github.com/webpack/less-loader#webpackimporter 245 | Details: 246 | * options.webpackImporter should be a boolean. 247 | * options.webpackImporter should be "only"." 248 | `; 249 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | 7 | [![npm][npm]][npm-url] 8 | [![node][node]][node-url] 9 | [![tests][tests]][tests-url] 10 | [![cover][cover]][cover-url] 11 | [![discussion][discussion]][discussion-url] 12 | [![size][size]][size-url] 13 | 14 | # less-loader 15 | 16 | A Less loader for webpack that compiles Less files into CSS. 17 | 18 | ## Getting Started 19 | 20 | To begin, you'll need to install `less` and `less-loader`: 21 | 22 | ```console 23 | npm install less less-loader --save-dev 24 | ``` 25 | 26 | or 27 | 28 | ```console 29 | yarn add -D less less-loader 30 | ``` 31 | 32 | or 33 | 34 | ```console 35 | pnpm add -D less less-loader 36 | ``` 37 | 38 | Then add the loader to your `webpack` configuration. For example: 39 | 40 | **webpack.config.js** 41 | 42 | ```js 43 | module.exports = { 44 | module: { 45 | rules: [ 46 | { 47 | test: /\.less$/i, 48 | use: [ 49 | // compiles Less to CSS 50 | "style-loader", 51 | "css-loader", 52 | "less-loader", 53 | ], 54 | }, 55 | ], 56 | }, 57 | }; 58 | ``` 59 | 60 | Finally, run `webpack` using the method you normally use (e.g., via CLI or an npm script). 61 | 62 | ## Options 63 | 64 | - **[`lessOptions`](#lessoptions)** 65 | - **[`additionalData`](#additionalData)** 66 | - **[`sourceMap`](#sourcemap)** 67 | - **[`webpackImporter`](#webpackimporter)** 68 | - **[`implementation`](#implementation)** 69 | - **[`lessLogAsWarnOrErr`](#lesslogaswarnorerr)** 70 | 71 | ### `lessOptions` 72 | 73 | Type: 74 | 75 | 76 | 77 | ```ts 78 | type lessOptions = import('less').options | ((loaderContext: LoaderContext) => import('less').options}) 79 | ``` 80 | 81 | Default: `{ relativeUrls: true }` 82 | 83 | You can pass any Less specific options to the `less-loader` through the `lessOptions` property in the [loader options](https://webpack.js.org/configuration/module/#ruleoptions--rulequery). See the [Less documentation](http://lesscss.org/usage/#command-line-usage-options) for all available options in dash-case. 84 | 85 | Since we're passing these options to Less programmatically, you need to pass them in camelCase here: 86 | 87 | #### `object` 88 | 89 | Use an object to pass options directly to Less. 90 | 91 | **webpack.config.js** 92 | 93 | ```js 94 | module.exports = { 95 | module: { 96 | rules: [ 97 | { 98 | test: /\.less$/i, 99 | use: [ 100 | { 101 | loader: "style-loader", 102 | }, 103 | { 104 | loader: "css-loader", 105 | }, 106 | { 107 | loader: "less-loader", 108 | options: { 109 | lessOptions: { 110 | strictMath: true, 111 | }, 112 | }, 113 | }, 114 | ], 115 | }, 116 | ], 117 | }, 118 | }; 119 | ``` 120 | 121 | #### `function` 122 | 123 | Allows setting the Less options dynamically based on the loader context. 124 | 125 | ```js 126 | module.exports = { 127 | module: { 128 | rules: [ 129 | { 130 | test: /\.less$/i, 131 | use: [ 132 | "style-loader", 133 | "css-loader", 134 | { 135 | loader: "less-loader", 136 | options: { 137 | lessOptions: (loaderContext) => { 138 | // More information about available properties https://webpack.js.org/api/loaders/ 139 | const { resourcePath, rootContext } = loaderContext; 140 | const relativePath = path.relative(rootContext, resourcePath); 141 | 142 | if (relativePath === "styles/foo.less") { 143 | return { 144 | paths: ["absolute/path/c", "absolute/path/d"], 145 | }; 146 | } 147 | 148 | return { 149 | paths: ["absolute/path/a", "absolute/path/b"], 150 | }; 151 | }, 152 | }, 153 | }, 154 | ], 155 | }, 156 | ], 157 | }, 158 | }; 159 | ``` 160 | 161 | ### `additionalData` 162 | 163 | Type: 164 | 165 | ```ts 166 | type additionalData = 167 | | string 168 | | ((content: string, loaderContext: LoaderContext) => string); 169 | ``` 170 | 171 | Default: `undefined` 172 | 173 | Prepends or Appends `Less` code to the actual entry file. 174 | In this case, the `less-loader` will not override the source but just **prepend** the entry's content. 175 | 176 | This is especially useful when some of your Less variables depend on the environment. 177 | 178 | > Since you're injecting code, this will break the source mappings in your entry file. Often there's a simpler solution than this, like multiple Less entry files. 179 | 180 | #### `string` 181 | 182 | ```js 183 | module.exports = { 184 | module: { 185 | rules: [ 186 | { 187 | test: /\.less$/i, 188 | use: [ 189 | "style-loader", 190 | "css-loader", 191 | { 192 | loader: "less-loader", 193 | options: { 194 | additionalData: `@env: ${process.env.NODE_ENV};`, 195 | }, 196 | }, 197 | ], 198 | }, 199 | ], 200 | }, 201 | }; 202 | ``` 203 | 204 | #### `function` 205 | 206 | ##### `Sync` 207 | 208 | ```js 209 | module.exports = { 210 | module: { 211 | rules: [ 212 | { 213 | test: /\.less$/i, 214 | use: [ 215 | "style-loader", 216 | "css-loader", 217 | { 218 | loader: "less-loader", 219 | options: { 220 | additionalData: (content, loaderContext) => { 221 | // More information about available properties https://webpack.js.org/api/loaders/ 222 | const { resourcePath, rootContext } = loaderContext; 223 | const relativePath = path.relative(rootContext, resourcePath); 224 | 225 | if (relativePath === "styles/foo.less") { 226 | return `@value: 100px;${content}`; 227 | } 228 | 229 | return `@value: 200px;${content}`; 230 | }, 231 | }, 232 | }, 233 | ], 234 | }, 235 | ], 236 | }, 237 | }; 238 | ``` 239 | 240 | ##### `Async` 241 | 242 | ```js 243 | module.exports = { 244 | module: { 245 | rules: [ 246 | { 247 | test: /\.less$/i, 248 | use: [ 249 | "style-loader", 250 | "css-loader", 251 | { 252 | loader: "less-loader", 253 | options: { 254 | additionalData: async (content, loaderContext) => { 255 | // More information about available properties https://webpack.js.org/api/loaders/ 256 | const { resourcePath, rootContext } = loaderContext; 257 | const relativePath = path.relative(rootContext, resourcePath); 258 | 259 | if (relativePath === "styles/foo.less") { 260 | return `@value: 100px;${content}`; 261 | } 262 | 263 | return `@value: 200px;${content}`; 264 | }, 265 | }, 266 | }, 267 | ], 268 | }, 269 | ], 270 | }, 271 | }; 272 | ``` 273 | 274 | ### `sourceMap` 275 | 276 | Type: 277 | 278 | ```ts 279 | type sourceMap = boolean; 280 | ``` 281 | 282 | Default: depends on the `compiler.devtool` value 283 | 284 | By default generation of source maps depends on the [`devtool`](https://webpack.js.org/configuration/devtool/) option. 285 | All values enable source map generation except `eval` and `false` value. 286 | 287 | **webpack.config.js** 288 | 289 | ```js 290 | module.exports = { 291 | module: { 292 | rules: [ 293 | { 294 | test: /\.less$/i, 295 | use: [ 296 | "style-loader", 297 | { 298 | loader: "css-loader", 299 | options: { 300 | sourceMap: true, 301 | }, 302 | }, 303 | { 304 | loader: "less-loader", 305 | options: { 306 | sourceMap: true, 307 | }, 308 | }, 309 | ], 310 | }, 311 | ], 312 | }, 313 | }; 314 | ``` 315 | 316 | ### `webpackImporter` 317 | 318 | Type: 319 | 320 | ```ts 321 | type webpackImporter = boolean | "only"; 322 | ``` 323 | 324 | Default: `true` 325 | 326 | Enables or disables the default `webpack` importer. 327 | 328 | This can improve performance in some cases. Use it with caution because aliases and `@import` from [`node_modules`](https://webpack.js.org/configuration/resolve/#resolvemodules) will not work. 329 | 330 | **webpack.config.js** 331 | 332 | ```js 333 | module.exports = { 334 | module: { 335 | rules: [ 336 | { 337 | test: /\.less$/i, 338 | use: [ 339 | "style-loader", 340 | "css-loader", 341 | { 342 | loader: "less-loader", 343 | options: { 344 | webpackImporter: false, 345 | }, 346 | }, 347 | ], 348 | }, 349 | ], 350 | }, 351 | }; 352 | ``` 353 | 354 | ### `implementation` 355 | 356 | Type: 357 | 358 | ```ts 359 | type implementation = object | string; 360 | ``` 361 | 362 | > less-loader compatible with both Less 3 and 4 versions 363 | 364 | The special `implementation` option determines which implementation of Less to use. Overrides the locally installed `peerDependency` version of `less`. 365 | 366 | **This option is only really useful for downstream tooling authors to ease the Less 3-to-4 transition.** 367 | 368 | #### `object` 369 | 370 | Example using a Less instance: 371 | 372 | **webpack.config.js** 373 | 374 | ```js 375 | module.exports = { 376 | module: { 377 | rules: [ 378 | { 379 | test: /\.less$/i, 380 | use: [ 381 | "style-loader", 382 | "css-loader", 383 | { 384 | loader: "less-loader", 385 | options: { 386 | implementation: require("less"), 387 | }, 388 | }, 389 | ], 390 | }, 391 | ], 392 | }, 393 | }; 394 | ``` 395 | 396 | #### `string` 397 | 398 | Example using a resolved Less module path: 399 | 400 | **webpack.config.js** 401 | 402 | ```js 403 | module.exports = { 404 | module: { 405 | rules: [ 406 | { 407 | test: /\.less$/i, 408 | use: [ 409 | "style-loader", 410 | "css-loader", 411 | { 412 | loader: "less-loader", 413 | options: { 414 | implementation: require.resolve("less"), 415 | }, 416 | }, 417 | ], 418 | }, 419 | ], 420 | }, 421 | }; 422 | ``` 423 | 424 | ### `lessLogAsWarnOrErr` 425 | 426 | Type: 427 | 428 | ```ts 429 | type lessLogAsWarnOrErr = boolean; 430 | ``` 431 | 432 | Default: `false` 433 | 434 | `Less` warnings and errors will be treated as webpack warnings and errors, instead of being logged silently. 435 | 436 | **warning.less** 437 | 438 | ```less 439 | div { 440 | &:extend(.body1); 441 | } 442 | ``` 443 | 444 | If `lessLogAsWarnOrErr` is set to `false` it will be just a log and webpack will compile successfully, but if you set this option to `true` webpack will compile fail with a warning(or error), and can break the build if configured accordingly. 445 | 446 | **webpack.config.js** 447 | 448 | ```js 449 | module.exports = { 450 | module: { 451 | rules: [ 452 | { 453 | test: /\.less$/i, 454 | use: [ 455 | "style-loader", 456 | "css-loader", 457 | { 458 | loader: "less-loader", 459 | options: { 460 | lessLogAsWarnOrErr: true, 461 | }, 462 | }, 463 | ], 464 | }, 465 | ], 466 | }, 467 | }; 468 | ``` 469 | 470 | ## Examples 471 | 472 | ### Normal usage 473 | 474 | Chain the `less-loader` with [`css-loader`](https://github.com/webpack/css-loader) and [`style-loader`](https://github.com/webpack/style-loader) to immediately apply all styles to the DOM. 475 | 476 | **webpack.config.js** 477 | 478 | ```js 479 | module.exports = { 480 | module: { 481 | rules: [ 482 | { 483 | test: /\.less$/i, 484 | use: [ 485 | { 486 | loader: "style-loader", // Creates style nodes from JS strings 487 | }, 488 | { 489 | loader: "css-loader", // Translates CSS into CommonJS 490 | }, 491 | { 492 | loader: "less-loader", // Compiles Less to CSS 493 | }, 494 | ], 495 | }, 496 | ], 497 | }, 498 | }; 499 | ``` 500 | 501 | Unfortunately, Less doesn't map all options 1-by-1 to camelCase. When in doubt, [check their executable](https://github.com/less/less.js/blob/3.x/bin/lessc) and search for the dash-case option. 502 | 503 | ### Source maps 504 | 505 | To enable sourcemaps for CSS, you'll need to pass the `sourceMap` property in the loader's options. If this is not passed, the loader will respect the setting for webpack source maps, set in `devtool`. 506 | 507 | **webpack.config.js** 508 | 509 | ```js 510 | module.exports = { 511 | devtool: "source-map", // any "source-map"-like devtool is possible 512 | module: { 513 | rules: [ 514 | { 515 | test: /\.less$/i, 516 | use: [ 517 | "style-loader", 518 | { 519 | loader: "css-loader", 520 | options: { 521 | sourceMap: true, 522 | }, 523 | }, 524 | { 525 | loader: "less-loader", 526 | options: { 527 | sourceMap: true, 528 | }, 529 | }, 530 | ], 531 | }, 532 | ], 533 | }, 534 | }; 535 | ``` 536 | 537 | If you want to edit the original Less files inside Chrome, [there's a good blog post](https://medium.com/@toolmantim/getting-started-with-css-sourcemaps-and-in-browser-sass-editing-b4daab987fb0). The blog post is about Sass but it also works for Less. 538 | 539 | ### In production 540 | 541 | Usually, it's recommended to extract the style sheets into a dedicated file in production using the [MiniCssExtractPlugin](https://github.com/webpack/mini-css-extract-plugin). This way your styles are not dependent on JavaScript, improving performance and cacheability. 542 | 543 | ### Imports 544 | 545 | First we try to use built-in `less` resolve logic, then `webpack` resolve logic. 546 | 547 | #### Webpack Resolver 548 | 549 | `webpack` provides an [advanced mechanism to resolve files](https://webpack.js.org/configuration/resolve/). 550 | `less-loader` applies a Less plugin that passes all queries to the webpack resolver if `less` could not resolve `@import`. 551 | Thus you can import your Less modules from `node_modules`. 552 | 553 | ```css 554 | @import "bootstrap/less/bootstrap"; 555 | ``` 556 | 557 | Using `~` prefix (e.g., @import "~bootstrap/less/bootstrap";) is deprecated and can be removed from your code (**we recommend it**), but we still support it for historical reasons. 558 | Why you can removed it? The loader will first try to resolve `@import` as relative, if it cannot be resolved, the loader will try to resolve `@import` inside [`node_modules`](https://webpack.js.org/configuration/resolve/#resolvemodules). 559 | 560 | Default resolver options can be modified by [`resolve.byDependency`](https://webpack.js.org/configuration/resolve/#resolvebydependency): 561 | 562 | **webpack.config.js** 563 | 564 | ```js 565 | module.exports = { 566 | devtool: "source-map", // any "source-map"-like devtool is possible 567 | module: { 568 | rules: [ 569 | { 570 | test: /\.less$/i, 571 | use: ["style-loader", "css-loader", "less-loader"], 572 | }, 573 | ], 574 | }, 575 | resolve: { 576 | byDependency: { 577 | // More options can be found here https://webpack.js.org/configuration/resolve/ 578 | less: { 579 | mainFiles: ["custom"], 580 | }, 581 | }, 582 | }, 583 | }; 584 | ``` 585 | 586 | #### Less Resolver 587 | 588 | If you specify the `paths` option, modules will be searched in the given `paths`. This is `less` default behavior. `paths` should be an array with absolute paths: 589 | 590 | **webpack.config.js** 591 | 592 | ```js 593 | module.exports = { 594 | module: { 595 | rules: [ 596 | { 597 | test: /\.less$/i, 598 | use: [ 599 | { 600 | loader: "style-loader", 601 | }, 602 | { 603 | loader: "css-loader", 604 | }, 605 | { 606 | loader: "less-loader", 607 | options: { 608 | lessOptions: { 609 | paths: [path.resolve(__dirname, "node_modules")], 610 | }, 611 | }, 612 | }, 613 | ], 614 | }, 615 | ], 616 | }, 617 | }; 618 | ``` 619 | 620 | ### Plugins 621 | 622 | In order to use [Less plugins](http://lesscss.org/usage/#plugins), simply set the `plugins` option like this: 623 | 624 | **webpack.config.js** 625 | 626 | 627 | 628 | ```js 629 | const CleanCSSPlugin = require('less-plugin-clean-css'); 630 | 631 | module.exports = { 632 | ... 633 | { 634 | loader: 'less-loader', 635 | options: { 636 | lessOptions: { 637 | plugins: [ 638 | new CleanCSSPlugin({ advanced: true }), 639 | ], 640 | }, 641 | }, 642 | }, 643 | ... 644 | }; 645 | ``` 646 | 647 | > [!NOTE] 648 | > 649 | > Access to the [loader context](https://webpack.js.org/api/loaders/#the-loader-context) inside a custom plugin can be done using the `pluginManager.webpackLoaderContext` property. 650 | 651 | ```js 652 | module.exports = { 653 | install(less, pluginManager, functions) { 654 | functions.add( 655 | "pi", 656 | () => 657 | // Loader context is available in `pluginManager.webpackLoaderContext` 658 | 659 | Math.PI, 660 | ); 661 | }, 662 | }; 663 | ``` 664 | 665 | ### Extracting style sheets 666 | 667 | Bundling CSS with webpack has some nice advantages like referencing images and fonts with hashed urls or [Hot Module Replacement(HMR)](https://webpack.js.org/concepts/hot-module-replacement/) in development. 668 | 669 | In production, on the other hand, it's not a good idea to apply your style sheets depending on JS execution. Rendering may be delayed or even a [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) might be visible. Thus it's often still better to have them as separate files in your final production build. 670 | 671 | There are two possibilities to extract a style sheet from the bundle: 672 | 673 | - [`extract-loader`](https://github.com/peerigon/extract-loader) (simpler, but specialized on the css-loader's output) 674 | - [`MiniCssExtractPlugin`](https://github.com/webpack/mini-css-extract-plugin) (more complex, but works in all use-cases) 675 | 676 | ### CSS modules gotcha 677 | 678 | There is a known problem when using Less with [CSS modules](https://github.com/css-modules/css-modules) regarding relative file paths in `url(...)` statements. 679 | [See this issue for an explanation](https://github.com/webpack/less-loader/issues/109#issuecomment-253797335). 680 | 681 | ## Contributing 682 | 683 | We welcome all contributions! 684 | If you're new here, please take a moment to review our contributing guidelines before submitting issues or pull requests. 685 | 686 | [CONTRIBUTING](https://github.com/webpack/less-loader?tab=contributing-ov-file#contributing) 687 | 688 | ## License 689 | 690 | [MIT](./LICENSE) 691 | 692 | [npm]: https://img.shields.io/npm/v/less-loader.svg 693 | [npm-url]: https://npmjs.com/package/less-loader 694 | [node]: https://img.shields.io/node/v/less-loader.svg 695 | [node-url]: https://nodejs.org 696 | [tests]: https://github.com/webpack/less-loader/workflows/less-loader/badge.svg 697 | [tests-url]: https://github.com/webpack/less-loader/actions 698 | [cover]: https://codecov.io/gh/webpack/less-loader/branch/main/graph/badge.svg 699 | [cover-url]: https://codecov.io/gh/webpack/less-loader 700 | [discussion]: https://img.shields.io/github/discussions/webpack/webpack 701 | [discussion-url]: https://github.com/webpack/webpack/discussions 702 | [size]: https://packagephobia.now.sh/badge?p=less-loader 703 | [size-url]: https://packagephobia.now.sh/result?p=less-loader 704 | -------------------------------------------------------------------------------- /test/__snapshots__/loader.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`loader should add a file with an error as dependency so that the watcher is triggered when the error is fixed: errors 1`] = ` 4 | [ 5 | "ModuleBuildError: Module build failed (from \`replaced original path\`): 6 | ", 7 | ] 8 | `; 9 | 10 | exports[`loader should add a file with an error as dependency so that the watcher is triggered when the error is fixed: warnings 1`] = `[]`; 11 | 12 | exports[`loader should add all resolved imports as dependencies, including aliased ones: errors 1`] = `[]`; 13 | 14 | exports[`loader should add all resolved imports as dependencies, including aliased ones: warnings 1`] = `[]`; 15 | 16 | exports[`loader should add all resolved imports as dependencies, including node_modules: errors 1`] = `[]`; 17 | 18 | exports[`loader should add all resolved imports as dependencies, including node_modules: warnings 1`] = `[]`; 19 | 20 | exports[`loader should add all resolved imports as dependencies, including those from the Less resolver: errors 1`] = `[]`; 21 | 22 | exports[`loader should add all resolved imports as dependencies, including those from the Less resolver: warnings 1`] = `[]`; 23 | 24 | exports[`loader should add all resolved imports as dependencies: errors 1`] = `[]`; 25 | 26 | exports[`loader should add all resolved imports as dependencies: warnings 1`] = `[]`; 27 | 28 | exports[`loader should add path to dependencies: css 1`] = ` 29 | ".box { 30 | color: #fe33ac; 31 | border-color: #fdcdea; 32 | background: url(box.png); 33 | } 34 | .box div { 35 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 36 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 37 | } 38 | " 39 | `; 40 | 41 | exports[`loader should add path to dependencies: errors 1`] = `[]`; 42 | 43 | exports[`loader should add path to dependencies: warnings 1`] = `[]`; 44 | 45 | exports[`loader should allow to import non-less files: css 1`] = ` 46 | ".some-file { 47 | background: hotpink; 48 | } 49 | " 50 | `; 51 | 52 | exports[`loader should allow to import non-less files: errors 1`] = `[]`; 53 | 54 | exports[`loader should allow to import non-less files: warnings 1`] = `[]`; 55 | 56 | exports[`loader should be able to import a file with an absolute path: css 1`] = ` 57 | ".it-works { 58 | color: yellow; 59 | } 60 | " 61 | `; 62 | 63 | exports[`loader should be able to import a file with an absolute path: errors 1`] = `[]`; 64 | 65 | exports[`loader should be able to import a file with an absolute path: warnings 1`] = `[]`; 66 | 67 | exports[`loader should compile data-uri function: css 1`] = ` 68 | ".img { 69 | background: url("data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%3E%0A%20%20%20%20%3Ccircle%20cx%3D%2220%22%20cy%3D%2220%22%20r%3D%2210%22%2F%3E%0A%3C%2Fsvg%3E"); 70 | } 71 | " 72 | `; 73 | 74 | exports[`loader should compile data-uri function: errors 1`] = `[]`; 75 | 76 | exports[`loader should compile data-uri function: warnings 1`] = `[]`; 77 | 78 | exports[`loader should emit an error: errors 1`] = ` 79 | [ 80 | "ModuleBuildError: Module build failed (from \`replaced original path\`): 81 | ", 82 | ] 83 | `; 84 | 85 | exports[`loader should emit an error: warnings 1`] = `[]`; 86 | 87 | exports[`loader should emit less warning as webpack warning: css 1`] = `""`; 88 | 89 | exports[`loader should emit less warning as webpack warning: errors 1`] = `[]`; 90 | 91 | exports[`loader should emit less warning as webpack warning: warnings 1`] = ` 92 | [ 93 | "ModuleWarning: Module Warning (from \`replaced original path\`): 94 | WARNING: extend ' .body1' has no matches", 95 | ] 96 | `; 97 | 98 | exports[`loader should get absolute path relative rootContext: css 1`] = ` 99 | ".box { 100 | color: #fe33ac; 101 | border-color: #fdcdea; 102 | background: url(box.png); 103 | } 104 | .box div { 105 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 106 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 107 | } 108 | " 109 | `; 110 | 111 | exports[`loader should get absolute path relative rootContext: errors 1`] = `[]`; 112 | 113 | exports[`loader should get absolute path relative rootContext: warnings 1`] = `[]`; 114 | 115 | exports[`loader should import from glob expressions: css 1`] = ` 116 | ".a { 117 | color: red; 118 | } 119 | " 120 | `; 121 | 122 | exports[`loader should import from glob expressions: errors 1`] = `[]`; 123 | 124 | exports[`loader should import from glob expressions: warnings 1`] = `[]`; 125 | 126 | exports[`loader should import from plugins: css 1`] = ` 127 | ".imported-class { 128 | color: coral; 129 | } 130 | " 131 | `; 132 | 133 | exports[`loader should import from plugins: errors 1`] = `[]`; 134 | 135 | exports[`loader should import from plugins: warnings 1`] = `[]`; 136 | 137 | exports[`loader should install plugins: errors 1`] = `[]`; 138 | 139 | exports[`loader should install plugins: warnings 1`] = `[]`; 140 | 141 | exports[`loader should not alter the original options object: errors 1`] = `[]`; 142 | 143 | exports[`loader should not alter the original options object: warnings 1`] = `[]`; 144 | 145 | exports[`loader should not to disable webpack's resolver by passing an empty paths array: css 1`] = ` 146 | ".img { 147 | background: url(some/img.jpg); 148 | } 149 | .img2 { 150 | background: url(../img.jpg); 151 | } 152 | .box { 153 | color: #fe33ac; 154 | border-color: #fdcdea; 155 | background: url(box.png); 156 | } 157 | .box div { 158 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 159 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 160 | } 161 | body { 162 | background: url(assets/resources/circle.svg); 163 | } 164 | .abs { 165 | background: url(assets/resources/circle.svg); 166 | } 167 | " 168 | `; 169 | 170 | exports[`loader should not to disable webpack's resolver by passing an empty paths array: errors 1`] = `[]`; 171 | 172 | exports[`loader should not to disable webpack's resolver by passing an empty paths array: warnings 1`] = `[]`; 173 | 174 | exports[`loader should not try to resolve CSS imports with URLs: css 1`] = ` 175 | "@import url("http://fonts.googleapis.com/css?family=Roboto:300,400,500"); 176 | @import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500"); 177 | @import url("//fonts.googleapis.com/css?family=Roboto:300,400,500"); 178 | " 179 | `; 180 | 181 | exports[`loader should not try to resolve CSS imports with URLs: errors 1`] = `[]`; 182 | 183 | exports[`loader should not try to resolve CSS imports with URLs: warnings 1`] = `[]`; 184 | 185 | exports[`loader should prefer-relative imports correctly: css 1`] = ` 186 | ".prefer-relative-import { 187 | background: red; 188 | } 189 | .relative { 190 | color: coral; 191 | } 192 | " 193 | `; 194 | 195 | exports[`loader should prefer-relative imports correctly: errors 1`] = `[]`; 196 | 197 | exports[`loader should prefer-relative imports correctly: warnings 1`] = `[]`; 198 | 199 | exports[`loader should provide a useful error message if the import could not be found: errors 1`] = ` 200 | [ 201 | "ModuleBuildError: Module build failed (from \`replaced original path\`): 202 | ", 203 | ] 204 | `; 205 | 206 | exports[`loader should provide a useful error message if the import could not be found: warnings 1`] = `[]`; 207 | 208 | exports[`loader should provide a useful error message if there was a syntax error: errors 1`] = ` 209 | [ 210 | "ModuleBuildError: Module build failed (from \`replaced original path\`): 211 | ", 212 | ] 213 | `; 214 | 215 | exports[`loader should provide a useful error message if there was a syntax error: warnings 1`] = `[]`; 216 | 217 | exports[`loader should resolve "@import" with "css" extension: css 1`] = ` 218 | "@import "css.css"; 219 | " 220 | `; 221 | 222 | exports[`loader should resolve "@import" with "css" extension: errors 1`] = `[]`; 223 | 224 | exports[`loader should resolve "@import" with "css" extension: warnings 1`] = `[]`; 225 | 226 | exports[`loader should resolve "@import" with "less" extension: css 1`] = ` 227 | ".box { 228 | color: #fe33ac; 229 | border-color: #fdcdea; 230 | background: url(box.png); 231 | } 232 | .box div { 233 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 234 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 235 | } 236 | " 237 | `; 238 | 239 | exports[`loader should resolve "@import" with "less" extension: errors 1`] = `[]`; 240 | 241 | exports[`loader should resolve "@import" with "less" extension: warnings 1`] = `[]`; 242 | 243 | exports[`loader should resolve "@import" with "php" extension: css 1`] = ` 244 | "a { 245 | color: red; 246 | } 247 | " 248 | `; 249 | 250 | exports[`loader should resolve "@import" with "php" extension: errors 1`] = `[]`; 251 | 252 | exports[`loader should resolve "@import" with "php" extension: warnings 1`] = `[]`; 253 | 254 | exports[`loader should resolve "@import" without "less" extension: css 1`] = ` 255 | ".box { 256 | color: #fe33ac; 257 | border-color: #fdcdea; 258 | background: url(box.png); 259 | } 260 | .box div { 261 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 262 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 263 | } 264 | " 265 | `; 266 | 267 | exports[`loader should resolve "@import" without "less" extension: errors 1`] = `[]`; 268 | 269 | exports[`loader should resolve "@import" without "less" extension: warnings 1`] = `[]`; 270 | 271 | exports[`loader should resolve absolute path with alias: css 1`] = ` 272 | ".box { 273 | color: #fe33ac; 274 | border-color: #fdcdea; 275 | background: url(box.png); 276 | } 277 | .box div { 278 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 279 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 280 | } 281 | " 282 | `; 283 | 284 | exports[`loader should resolve absolute path with alias: errors 1`] = `[]`; 285 | 286 | exports[`loader should resolve absolute path with alias: warnings 1`] = `[]`; 287 | 288 | exports[`loader should resolve absolute path: css 1`] = ` 289 | ".box { 290 | color: #fe33ac; 291 | border-color: #fdcdea; 292 | background: url(box.png); 293 | } 294 | .box div { 295 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 296 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 297 | } 298 | " 299 | `; 300 | 301 | exports[`loader should resolve absolute path: errors 1`] = `[]`; 302 | 303 | exports[`loader should resolve absolute path: warnings 1`] = `[]`; 304 | 305 | exports[`loader should resolve aliases in different variants: css 1`] = ` 306 | ".img { 307 | background: url(some/img.jpg); 308 | } 309 | .img2 { 310 | background: url(../img.jpg); 311 | } 312 | .box { 313 | color: #fe33ac; 314 | border-color: #fdcdea; 315 | background: url(box.png); 316 | } 317 | .box div { 318 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 319 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 320 | } 321 | body { 322 | background: url(assets/resources/circle.svg); 323 | } 324 | .abs { 325 | background: url(assets/resources/circle.svg); 326 | } 327 | " 328 | `; 329 | 330 | exports[`loader should resolve aliases in different variants: errors 1`] = `[]`; 331 | 332 | exports[`loader should resolve aliases in different variants: warnings 1`] = `[]`; 333 | 334 | exports[`loader should resolve all imports from node_modules using webpack's resolver: css 1`] = ` 335 | "@import "some/css.css"; 336 | @import "some/css.css"; 337 | #it-works { 338 | color: hotpink; 339 | } 340 | .modules-dir-some-module, 341 | #it-works { 342 | background: hotpink; 343 | } 344 | #it-works { 345 | margin: 10px; 346 | } 347 | " 348 | `; 349 | 350 | exports[`loader should resolve all imports from node_modules using webpack's resolver: errors 1`] = `[]`; 351 | 352 | exports[`loader should resolve all imports from node_modules using webpack's resolver: warnings 1`] = `[]`; 353 | 354 | exports[`loader should resolve all imports from the given paths using Less resolver: css 1`] = ` 355 | ".modules-dir-some-module { 356 | color: hotpink; 357 | } 358 | " 359 | `; 360 | 361 | exports[`loader should resolve all imports from the given paths using Less resolver: errors 1`] = `[]`; 362 | 363 | exports[`loader should resolve all imports from the given paths using Less resolver: warnings 1`] = `[]`; 364 | 365 | exports[`loader should resolve all imports: css 1`] = ` 366 | "@import "css.css"; 367 | @import "css.css"; 368 | .classical-css, 369 | #it-works { 370 | background: hotpink; 371 | } 372 | .box, 373 | #it-works { 374 | color: #fe33ac; 375 | border-color: #fdcdea; 376 | background: url(box.png); 377 | } 378 | .box div { 379 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 380 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 381 | } 382 | #it-works { 383 | margin: 10px; 384 | } 385 | " 386 | `; 387 | 388 | exports[`loader should resolve all imports: errors 1`] = `[]`; 389 | 390 | exports[`loader should resolve all imports: warnings 1`] = `[]`; 391 | 392 | exports[`loader should resolve in working directory: css 1`] = ` 393 | ".test { 394 | color: red; 395 | } 396 | a { 397 | color: red; 398 | } 399 | body { 400 | margin: 0; 401 | } 402 | " 403 | `; 404 | 405 | exports[`loader should resolve in working directory: errors 1`] = `[]`; 406 | 407 | exports[`loader should resolve in working directory: warnings 1`] = `[]`; 408 | 409 | exports[`loader should resolve nested imports: css 1`] = ` 410 | ".top-import { 411 | background: red; 412 | } 413 | .nested-import { 414 | background: coral; 415 | } 416 | " 417 | `; 418 | 419 | exports[`loader should resolve nested imports: errors 1`] = `[]`; 420 | 421 | exports[`loader should resolve nested imports: warnings 1`] = `[]`; 422 | 423 | exports[`loader should resolve nested package #2: css 1`] = ` 424 | ".less-package-1-nested { 425 | background: red; 426 | } 427 | .less-package-2 { 428 | background: red; 429 | } 430 | .top { 431 | color: red; 432 | } 433 | " 434 | `; 435 | 436 | exports[`loader should resolve nested package #2: errors 1`] = `[]`; 437 | 438 | exports[`loader should resolve nested package #2: warnings 1`] = `[]`; 439 | 440 | exports[`loader should resolve nested package: css 1`] = ` 441 | ".less-package-1-nested { 442 | background: red; 443 | } 444 | .less-package-2 { 445 | background: red; 446 | } 447 | " 448 | `; 449 | 450 | exports[`loader should resolve nested package: errors 1`] = `[]`; 451 | 452 | exports[`loader should resolve nested package: warnings 1`] = `[]`; 453 | 454 | exports[`loader should resolve non-less import with alias: css 1`] = ` 455 | ".some-file { 456 | background: hotpink; 457 | } 458 | " 459 | `; 460 | 461 | exports[`loader should resolve non-less import with alias: errors 1`] = `[]`; 462 | 463 | exports[`loader should resolve non-less import with alias: warnings 1`] = `[]`; 464 | 465 | exports[`loader should resolve the "less" field from the "exports" field from "package.json": css 1`] = ` 466 | ".load-me { 467 | color: red; 468 | } 469 | " 470 | `; 471 | 472 | exports[`loader should resolve the "less" field from the "exports" field from "package.json": errors 1`] = `[]`; 473 | 474 | exports[`loader should resolve the "less" field from the "exports" field from "package.json": warnings 1`] = `[]`; 475 | 476 | exports[`loader should resolve unresolved url with alias: css 1`] = ` 477 | ".box { 478 | color: #fe33ac; 479 | border-color: #fdcdea; 480 | background: url(box.png); 481 | } 482 | .box div { 483 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 484 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 485 | } 486 | " 487 | `; 488 | 489 | exports[`loader should resolve unresolved url with alias: errors 1`] = `[]`; 490 | 491 | exports[`loader should resolve unresolved url with alias: warnings 1`] = `[]`; 492 | 493 | exports[`loader should throw an error: errors 1`] = ` 494 | [ 495 | "ModuleBuildError: Module build failed (from \`replaced original path\`): 496 | ", 497 | ] 498 | `; 499 | 500 | exports[`loader should throw an error: warnings 1`] = `[]`; 501 | 502 | exports[`loader should transform urls: css 1`] = ` 503 | ".img4 { 504 | background: url(folder/img.jpg); 505 | } 506 | .img5 { 507 | background: url(folder/some/img.jpg); 508 | } 509 | .img6 { 510 | background: url(./img.jpg); 511 | } 512 | .img1 { 513 | background: url(img.jpg); 514 | } 515 | .img2 { 516 | background: url(some/img.jpg); 517 | } 518 | .img3 { 519 | background: url(../img.jpg); 520 | } 521 | " 522 | `; 523 | 524 | exports[`loader should transform urls: errors 1`] = `[]`; 525 | 526 | exports[`loader should transform urls: warnings 1`] = `[]`; 527 | 528 | exports[`loader should watch imports correctly: css 1`] = ` 529 | "a { 530 | color: red; 531 | } 532 | " 533 | `; 534 | 535 | exports[`loader should watch imports correctly: errors 1`] = `[]`; 536 | 537 | exports[`loader should watch imports correctly: warnings 1`] = `[]`; 538 | 539 | exports[`loader should work and have loaderContext in less plugins: css 1`] = ` 540 | ".webpackLoaderContext { 541 | isDefined: true; 542 | } 543 | " 544 | `; 545 | 546 | exports[`loader should work and have loaderContext in less plugins: errors 1`] = `[]`; 547 | 548 | exports[`loader should work and have loaderContext in less plugins: warnings 1`] = `[]`; 549 | 550 | exports[`loader should work and logging: css 1`] = ` 551 | "nav ul { 552 | background: blue; 553 | } 554 | " 555 | `; 556 | 557 | exports[`loader should work and logging: errors 1`] = `[]`; 558 | 559 | exports[`loader should work and logging: logs 1`] = ` 560 | [ 561 | [ 562 | { 563 | "args": [ 564 | "WARNING: extend ' .inline' has no matches", 565 | ], 566 | "type": "warn", 567 | }, 568 | { 569 | "args": [ 570 | "The file undefined was skipped because it was not found and the import was marked optional.", 571 | ], 572 | "type": "log", 573 | }, 574 | ], 575 | ] 576 | `; 577 | 578 | exports[`loader should work and logging: warnings 1`] = `[]`; 579 | 580 | exports[`loader should work and respect the 'resolve.byDependency.less' option: css 1`] = ` 581 | ".a { 582 | color: red; 583 | } 584 | .b { 585 | color: red; 586 | } 587 | " 588 | `; 589 | 590 | exports[`loader should work and respect the 'resolve.byDependency.less' option: errors 1`] = `[]`; 591 | 592 | exports[`loader should work and respect the 'resolve.byDependency.less' option: warnings 1`] = `[]`; 593 | 594 | exports[`loader should work lessOptions.relativeUrls is false: css 1`] = ` 595 | ".top-import { 596 | background: red; 597 | } 598 | .img4 { 599 | background: url(img.jpg); 600 | } 601 | .img5 { 602 | background: url(some/img.jpg); 603 | } 604 | .img6 { 605 | background: url(../img.jpg); 606 | } 607 | " 608 | `; 609 | 610 | exports[`loader should work lessOptions.relativeUrls is false: errors 1`] = `[]`; 611 | 612 | exports[`loader should work lessOptions.relativeUrls is false: warnings 1`] = `[]`; 613 | 614 | exports[`loader should work lessOptions.relativeUrls is true: css 1`] = ` 615 | ".top-import { 616 | background: red; 617 | } 618 | .img4 { 619 | background: url(folder/img.jpg); 620 | } 621 | .img5 { 622 | background: url(folder/some/img.jpg); 623 | } 624 | .img6 { 625 | background: url(./img.jpg); 626 | } 627 | " 628 | `; 629 | 630 | exports[`loader should work lessOptions.relativeUrls is true: errors 1`] = `[]`; 631 | 632 | exports[`loader should work lessOptions.relativeUrls is true: warnings 1`] = `[]`; 633 | 634 | exports[`loader should work third-party plugins as fileLoader: css 1`] = ` 635 | ".file-loader { 636 | background: coral; 637 | } 638 | @font-face { 639 | font-family: 'Roboto'; 640 | font-style: normal; 641 | font-weight: 500; 642 | src: url(https://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmEU9fBBc9.ttf) format('truetype'); 643 | } 644 | .modules-dir-scope-module { 645 | color: hotpink; 646 | } 647 | " 648 | `; 649 | 650 | exports[`loader should work third-party plugins as fileLoader: errors 1`] = `[]`; 651 | 652 | exports[`loader should work third-party plugins as fileLoader: warnings 1`] = `[]`; 653 | 654 | exports[`loader should work with a package with "sass" and "exports" fields and a custom condition (theme1): css 1`] = ` 655 | ".load-me { 656 | color: red; 657 | } 658 | " 659 | `; 660 | 661 | exports[`loader should work with a package with "sass" and "exports" fields and a custom condition (theme1): errors 1`] = `[]`; 662 | 663 | exports[`loader should work with a package with "sass" and "exports" fields and a custom condition (theme1): warnings 1`] = `[]`; 664 | 665 | exports[`loader should work with a package with "sass" and "exports" fields and a custom condition (theme2): css 1`] = ` 666 | ".load-me { 667 | color: blue; 668 | } 669 | " 670 | `; 671 | 672 | exports[`loader should work with a package with "sass" and "exports" fields and a custom condition (theme2): errors 1`] = `[]`; 673 | 674 | exports[`loader should work with a package with "sass" and "exports" fields and a custom condition (theme2): warnings 1`] = `[]`; 675 | 676 | exports[`loader should work: css 1`] = ` 677 | ".box { 678 | color: #fe33ac; 679 | border-color: #fdcdea; 680 | background: url(box.png); 681 | } 682 | .box div { 683 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 684 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 685 | } 686 | " 687 | `; 688 | 689 | exports[`loader should work: errors 1`] = `[]`; 690 | 691 | exports[`loader should work: warnings 1`] = `[]`; 692 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [12.3.0](https://github.com/webpack-contrib/less-loader/compare/v12.2.0...v12.3.0) (2025-05-01) 6 | 7 | 8 | ### Features 9 | 10 | * add support for using only webpackImporter ([12839c8](https://github.com/webpack-contrib/less-loader/commit/12839c8c2af52662b79c54021d903ce88e68f894)) 11 | 12 | ## [12.2.0](https://github.com/webpack-contrib/less-loader/compare/v12.1.0...v12.2.0) (2024-01-30) 13 | 14 | 15 | ### Features 16 | 17 | * add `@rspack/core` as an optional peer dependency ([#537](https://github.com/webpack-contrib/less-loader/issues/537)) ([71dd711](https://github.com/webpack-contrib/less-loader/commit/71dd711fd1ac796d5c7d972c61acfe5036df3a40)) 18 | 19 | ## [12.1.0](https://github.com/webpack-contrib/less-loader/compare/v12.0.0...v12.1.0) (2024-01-19) 20 | 21 | 22 | ### Features 23 | 24 | * added the `lessLogAsWarnOrErr` option ([#536](https://github.com/webpack-contrib/less-loader/issues/536)) ([3c4e6e0](https://github.com/webpack-contrib/less-loader/commit/3c4e6e0293e268b76a22c203024fdf248980a893)) 25 | 26 | ## [12.0.0](https://github.com/webpack-contrib/less-loader/compare/v11.1.4...v12.0.0) (2024-01-15) 27 | 28 | 29 | ### ⚠ BREAKING CHANGES 30 | 31 | * minimum supported Node.js version is `18.12.0` ([#533](https://github.com/webpack-contrib/less-loader/issues/533)) ([f70e832](https://github.com/webpack-contrib/less-loader/commit/f70e832485cc1b54bf2f57c9b707eb96aeaf52c8)) 32 | 33 | ### [11.1.4](https://github.com/webpack-contrib/less-loader/compare/v11.1.3...v11.1.4) (2023-12-27) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * invalid dependencies with working directory ([#531](https://github.com/webpack-contrib/less-loader/issues/531)) ([2ec31a6](https://github.com/webpack-contrib/less-loader/commit/2ec31a6e4725ed245c10253f10e60f7f222722a5)) 39 | 40 | ### [11.1.3](https://github.com/webpack-contrib/less-loader/compare/v11.1.2...v11.1.3) (2023-06-08) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * **perf:** avoid using `klona` for `less` options ([#520](https://github.com/webpack-contrib/less-loader/issues/520)) ([8a63159](https://github.com/webpack-contrib/less-loader/commit/8a6315985b63c1fbb6b31ada1824951a2d2fbaa8)) 46 | 47 | ### [11.1.2](https://github.com/webpack-contrib/less-loader/compare/v11.1.1...v11.1.2) (2023-05-31) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * remove unused `v` dependency ([#517](https://github.com/webpack-contrib/less-loader/issues/517)) ([8fd9206](https://github.com/webpack-contrib/less-loader/commit/8fd9206aa607ba989fdcccd8f1c000dfc65c9017)) 53 | 54 | ### [11.1.1](https://github.com/webpack-contrib/less-loader/compare/v11.1.0...v11.1.1) (2023-05-28) 55 | 56 | 57 | ### Bug Fixes 58 | 59 | * handling errors better ([#515](https://github.com/webpack-contrib/less-loader/issues/515)) ([5e0308e](https://github.com/webpack-contrib/less-loader/commit/5e0308e106feec767b44a5fa29696009f95d3a2a)) 60 | * make errors serializable ([#516](https://github.com/webpack-contrib/less-loader/issues/516)) ([68adcc2](https://github.com/webpack-contrib/less-loader/commit/68adcc27f88737db4739942ebd611591ec360b74)) 61 | 62 | ## [11.1.0](https://github.com/webpack-contrib/less-loader/compare/v11.0.0...v11.1.0) (2022-10-06) 63 | 64 | 65 | ### Features 66 | 67 | * allow to extend `conditionNames` ([#488](https://github.com/webpack-contrib/less-loader/issues/488)) ([43cd20c](https://github.com/webpack-contrib/less-loader/commit/43cd20c7c321c07d98df73bd16405f46f86cfc4f)) 68 | 69 | ## [11.0.0](https://github.com/webpack-contrib/less-loader/compare/v10.2.0...v11.0.0) (2022-05-17) 70 | 71 | 72 | ### ⚠ BREAKING CHANGES 73 | 74 | * minimum supported `Node.js` version is `14.15.0` 75 | 76 | ## [10.2.0](https://github.com/webpack-contrib/less-loader/compare/v10.1.0...v10.2.0) (2021-10-18) 77 | 78 | 79 | ### Features 80 | 81 | * use webpack logger for logging ([#444](https://github.com/webpack-contrib/less-loader/issues/444)) ([239c737](https://github.com/webpack-contrib/less-loader/commit/239c737e2ede1d17d83a6d11a6bd11211cf7d77d)) 82 | 83 | ## [10.1.0](https://github.com/webpack-contrib/less-loader/compare/v10.0.1...v10.1.0) (2021-10-11) 84 | 85 | 86 | ### Features 87 | 88 | * add `link` field in schema ([#429](https://github.com/webpack-contrib/less-loader/issues/429)) ([8580731](https://github.com/webpack-contrib/less-loader/commit/858073159cc5aef320e7682798bc987f93ef4817)) 89 | 90 | 91 | ### Bug Fixes 92 | 93 | * only call `addDependency` on absolute paths ([fa11ce7](https://github.com/webpack-contrib/less-loader/commit/fa11ce7670ed8cae484e3435be05713deb199954)) 94 | 95 | 96 | ### [10.0.1](https://github.com/webpack-contrib/less-loader/compare/v10.0.0...v10.0.1) (2021-07-02) 97 | 98 | 99 | ### Bug Fixes 100 | 101 | * memory leak ([#426](https://github.com/webpack-contrib/less-loader/issues/426)) ([d74f740](https://github.com/webpack-contrib/less-loader/commit/d74f740c100c4006b00dfb3e02c6d5aaf8713519)) 102 | 103 | ## [10.0.0](https://github.com/webpack-contrib/less-loader/compare/v9.1.0...v10.0.0) (2021-06-17) 104 | 105 | 106 | ### ⚠ BREAKING CHANGES 107 | 108 | * `less.webpackLoaderContext` was removed, please use `pluginManager.webpackLoaderContext` 109 | 110 | ### Bug Fixes 111 | 112 | * memory usage ([#425](https://github.com/webpack-contrib/less-loader/issues/425)) ([9c03b59](https://github.com/webpack-contrib/less-loader/commit/9c03b5914240e4e18bebd2d3a47ec0a650a67701)) 113 | 114 | ## [9.1.0](https://github.com/webpack-contrib/less-loader/compare/v9.0.0...v9.1.0) (2021-06-10) 115 | 116 | 117 | ### Features 118 | 119 | * allow to use `String` value for the `implementation` option ([465ffc4](https://github.com/webpack-contrib/less-loader/commit/465ffc4052642d799bb29a85056517db31ee1bf5)) 120 | 121 | ## [9.0.0](https://github.com/webpack-contrib/less-loader/compare/v8.1.1...v9.0.0) (2021-05-13) 122 | 123 | 124 | ### ⚠ BREAKING CHANGES 125 | 126 | * minimum supported `Node.js` version is `12.13.0` 127 | 128 | ### [8.1.1](https://github.com/webpack-contrib/less-loader/compare/v8.1.0...v8.1.1) (2021-04-15) 129 | 130 | 131 | ### Bug Fixes 132 | 133 | * deprecation warning ([#415](https://github.com/webpack-contrib/less-loader/issues/415)) ([87a4f25](https://github.com/webpack-contrib/less-loader/commit/87a4f25e2c8343ae2c75486749e57c2d10b1490d)) 134 | 135 | ## [8.1.0](https://github.com/webpack-contrib/less-loader/compare/v8.0.0...v8.1.0) (2021-04-09) 136 | 137 | 138 | ### Features 139 | 140 | * added the `pluginManager.webpackLoaderContext` property for `less` plugin developers, deprecated the `less.webpackLoaderContext` property, it fixed memory leak, please read [this](https://github.com/webpack-contrib/less-loader#plugins) ([#412](https://github.com/webpack-contrib/less-loader/issues/412)) ([e576240](https://github.com/webpack-contrib/less-loader/commit/e5762404093ec6246079c6b975c9f93c0a521bd9)) 141 | 142 | ## [8.0.0](https://github.com/webpack-contrib/less-loader/compare/v7.3.0...v8.0.0) (2021-02-01) 143 | 144 | 145 | ### Notes 146 | 147 | * using `~` is deprecated and can be removed from your code (**we recommend it**), but we still support it for historical reasons. 148 | 149 | Why you can removed it? 150 | The loader will first try to resolve `@import` as relative, if it cannot be resolved, the loader will try to resolve `@import` inside [`node_modules`](https://webpack.js.org/configuration/resolve/#resolve-modules). 151 | 152 | ### ⚠ BREAKING CHANGES 153 | 154 | * minimum supported `webpack` version is `5` 155 | 156 | ### Features 157 | 158 | * supported the [`resolve.byDependency`](https://webpack.js.org/configuration/resolve/#resolvebydependency) option, you can setup `{ resolve: { byDependency: { less: { mainFiles: ['custom', '...'] } } } }` 159 | 160 | ## [7.3.0](https://github.com/webpack-contrib/less-loader/compare/v7.2.1...v7.3.0) (2021-01-21) 161 | 162 | 163 | ### Features 164 | 165 | * added the `implementation` option ([84d957c](https://github.com/webpack-contrib/less-loader/commit/84d957cfe4fbd6c97619c647d0dd0917b99408ae)) 166 | 167 | ### [7.2.1](https://github.com/webpack-contrib/less-loader/compare/v7.2.0...v7.2.1) (2020-12-28) 168 | 169 | 170 | ### Bug Fixes 171 | 172 | * errors from less ([#401](https://github.com/webpack-contrib/less-loader/issues/401)) ([ce31aca](https://github.com/webpack-contrib/less-loader/commit/ce31aca7aada70a8cc267449954ab38b642cd4ba)) 173 | 174 | ## [7.2.0](https://github.com/webpack-contrib/less-loader/compare/v7.1.0...v7.2.0) (2020-12-23) 175 | 176 | 177 | ### Features 178 | 179 | * add less ^4.0 to peerDependencies ([#398](https://github.com/webpack-contrib/less-loader/issues/398)) ([3d1abb7](https://github.com/webpack-contrib/less-loader/commit/3d1abb7be041e44fce59b2109d02eada1451a4e4)) 180 | 181 | ## [7.1.0](https://github.com/webpack-contrib/less-loader/compare/v7.0.2...v7.1.0) (2020-11-11) 182 | 183 | 184 | ### Features 185 | 186 | * allow the `additionalData` to be async ([#391](https://github.com/webpack-contrib/less-loader/issues/391)) ([62c6934](https://github.com/webpack-contrib/less-loader/commit/62c6934367eb4dd0d4f3155ed2bb5f3e065aafba)) 187 | 188 | ### [7.0.2](https://github.com/webpack-contrib/less-loader/compare/v7.0.1...v7.0.2) (2020-10-09) 189 | 190 | ### Chore 191 | 192 | * update `schema-utils` 193 | 194 | ### [7.0.1](https://github.com/webpack-contrib/less-loader/compare/v7.0.0...v7.0.1) (2020-09-03) 195 | 196 | 197 | ### Bug Fixes 198 | 199 | * normalize `sources` in source maps ([877d99a](https://github.com/webpack-contrib/less-loader/commit/877d99a380deac92e07c41429a9b0c5f0bba2710)) 200 | 201 | ## [7.0.0](https://github.com/webpack-contrib/less-loader/compare/v6.2.0...v7.0.0) (2020-08-25) 202 | 203 | 204 | ### ⚠ BREAKING CHANGES 205 | 206 | * move `less` to `peerDependencies`, the `implementation` option was removed 207 | * `prependData` and `appendData` option were removed in favor the `additionaldata` option 208 | 209 | ### Features 210 | 211 | * added `webpackImporter` option ([#377](https://github.com/webpack-contrib/less-loader/issues/377)) ([12dca5b](https://github.com/webpack-contrib/less-loader/commit/12dca5bb573740472cff8176b7aade184c773ebc)) 212 | * added loader context in less plugins ([#378](https://github.com/webpack-contrib/less-loader/issues/378)) ([7b7fc5e](https://github.com/webpack-contrib/less-loader/commit/7b7fc5e841a2c7c587a980648056ed4762014e9d)) 213 | * added `additionaldata` option ([#374](https://github.com/webpack-contrib/less-loader/issues/374)) ([2785803](https://github.com/webpack-contrib/less-loader/commit/27858037a2e307fdf437604300f14c8233df4568)) 214 | 215 | ## [6.2.0](https://github.com/webpack-contrib/less-loader/compare/v6.1.3...v6.2.0) (2020-07-03) 216 | 217 | 218 | ### Features 219 | 220 | * support condition names from `package.json` ([#369](https://github.com/webpack-contrib/less-loader/issues/369)) ([671395d](https://github.com/webpack-contrib/less-loader/commit/671395d6a82425ba4408d1329d8cbfa07dfd9153)) 221 | 222 | ### [6.1.3](https://github.com/webpack-contrib/less-loader/compare/v6.1.2...v6.1.3) (2020-06-29) 223 | 224 | 225 | ### Bug Fixes 226 | 227 | * revert restrictions ([e758837](https://github.com/webpack-contrib/less-loader/commit/e75883706fc3d3bb2b6283a727a405216473362e)) 228 | 229 | ### [6.1.2](https://github.com/webpack-contrib/less-loader/compare/v6.1.1...v6.1.2) (2020-06-22) 230 | 231 | 232 | ### Bug Fixes 233 | 234 | * ignore watch for remove URLs ([3946937](https://github.com/webpack-contrib/less-loader/commit/39469376e28cd0e38162f7bdf8935d343830a40e)) 235 | * resolution logic ([2c3a23a](https://github.com/webpack-contrib/less-loader/commit/2c3a23a440cbdad1edb8b232864cb0233a266782)) 236 | * resolve absolute and root relative imports ([3d01b82](https://github.com/webpack-contrib/less-loader/commit/3d01b82fae335d5d69d6290911e788debc732182)) 237 | 238 | ### [6.1.1](https://github.com/webpack-contrib/less-loader/compare/v6.1.0...v6.1.1) (2020-06-11) 239 | 240 | 241 | ### Bug Fixes 242 | 243 | * do not rebuilt unmodified files on windows in watch mode ([6537a3d](https://github.com/webpack-contrib/less-loader/commit/6537a3d66559464af9b9a25f4bdda8691e8d9407)) 244 | 245 | ## [6.1.0](https://github.com/webpack-contrib/less-loader/compare/v6.0.0...v6.1.0) (2020-05-07) 246 | 247 | 248 | ### Features 249 | 250 | * new `implementation` option ([#354](https://github.com/webpack-contrib/less-loader/issues/354)) ([d2de80f](https://github.com/webpack-contrib/less-loader/commit/d2de80f9fe6ee11e784260dbda960853ebd2449b)) 251 | 252 | 253 | ### Bug Fixes 254 | 255 | * respect third-party plugins for `Less` ([#353](https://github.com/webpack-contrib/less-loader/issues/353)) ([d0db4f9](https://github.com/webpack-contrib/less-loader/commit/d0db4f9839c4921440c9a0fdc00fd00bc5a6fbb8)) 256 | 257 | ## [6.0.0](https://github.com/webpack-contrib/less-loader/compare/v5.0.0...v6.0.0) (2020-04-24) 258 | 259 | 260 | ### ⚠ BREAKING CHANGES 261 | 262 | * minimum supported Node.js version is `10.13`, 263 | * minimum support webpack version is `4` 264 | * `2` version of `less` is not supported anymore 265 | * using `3` versin of `less` by default, so you don't need to have `less` in your `package.json`, we already supply it 266 | * move less-specific options to the `lessOptions` option, please look at [README](https://github.com/webpack-contrib/less-loader#lessoptions) 267 | 268 | 269 | ### Features 270 | 271 | * the `paths` options now works with webpack resolver ([3931470](https://github.com/webpack-contrib/less-loader/commit/393147064672ace986ec84aca21f69f0ab819a9c)) 272 | * allow a function to be used for `lessOptions` ([#325](https://github.com/webpack-contrib/less-loader/issues/325)) ([a6be94a](https://github.com/webpack-contrib/less-loader/commit/a6be94a6da291a27026415d509249e0203e977ad)) 273 | * added the `appendData` option ([#336](https://github.com/webpack-contrib/less-loader/issues/336)) ([fb94605](https://github.com/webpack-contrib/less-loader/commit/fb946051bb4d52a6f9a93fe40a8cd09a56a2c5f1)) 274 | * added the `prependData` option ([#327](https://github.com/webpack-contrib/less-loader/issues/327)) ([9df8755](https://github.com/webpack-contrib/less-loader/commit/9df87554ee1ac57d2c32743049174da20e8a8a61)) 275 | * support `less` and `style` fields in `package.json` 276 | * support `index.less` file for packages 277 | 278 | ### Bug Fixes 279 | 280 | * support import aliases without tilde ([#335](https://github.com/webpack-contrib/less-loader/issues/335)) ([24021cd](https://github.com/webpack-contrib/less-loader/commit/24021cdb9dc0496fcebd6966516ff66584525cf3)) 281 | * do not crash on remotely imports ([#333](https://github.com/webpack-contrib/less-loader/issues/333)) ([8e020e9](https://github.com/webpack-contrib/less-loader/commit/8e020e9cf794d958024cc91ad490b621d5170878)) 282 | * add webpack v5 support ([#317](https://github.com/webpack-contrib/less-loader/issues/317)) ([f0b42b4](https://github.com/webpack-contrib/less-loader/commit/f0b42b4e64dceed0bbb2557c0d88d1c36fe3e553)) 283 | * first resolve an import using less resolver, then using webpack resolver ([#340](https://github.com/webpack-contrib/less-loader/issues/340)) ([443bd5a](https://github.com/webpack-contrib/less-loader/commit/443bd5ac0539ca93a998326754bcd607aaecdf1a)) 284 | * fix a resolution for `@import 'package/file.ess';` and `@import './package/file.ess';` 285 | 286 | 287 | 288 | # [5.0.0](https://github.com/webpack-contrib/less-loader/compare/v4.1.0...v5.0.0) (2019-04-29) 289 | 290 | 291 | ### Bug Fixes 292 | 293 | * webpack watching does not recover after broken less is fixed ([#289](https://github.com/webpack-contrib/less-loader/issues/289)) ([f41d12e](https://github.com/webpack-contrib/less-loader/commit/f41d12e)) 294 | 295 | 296 | ### Chores 297 | 298 | * remove old bits mentioning webpack < 4 and node < 6 ([#286](https://github.com/webpack-contrib/less-loader/issues/286)) ([012eb8f](https://github.com/webpack-contrib/less-loader/commit/012eb8f)) 299 | 300 | 301 | ### Code Refactoring 302 | 303 | * remove deprecated compress option ([#283](https://github.com/webpack-contrib/less-loader/issues/283)) ([3d6e9e9](https://github.com/webpack-contrib/less-loader/commit/3d6e9e9)) 304 | 305 | 306 | ### BREAKING CHANGES 307 | 308 | * remove deprecated compress option. 309 | * drop support for node < 6.9 and webpack < 4 310 | 311 | 312 | 313 | 314 | # [4.1.0](https://github.com/webpack-contrib/less-loader/compare/v4.0.6...v4.1.0) (2018-03-09) 315 | 316 | 317 | ### Features 318 | 319 | * **package:** support `less >= v3.0.0` ([#242](https://github.com/webpack-contrib/less-loader/issues/242)) ([d8c9d83](https://github.com/webpack-contrib/less-loader/commit/d8c9d83)) 320 | 321 | 322 | 323 | 324 | ## [4.0.6](https://github.com/webpack-contrib/less-loader/compare/v4.0.5...v4.0.6) (2018-02-27) 325 | 326 | 327 | ### Bug Fixes 328 | 329 | * **package:** add `webpack >= v4.0.0` (`peerDependencies`) ([#245](https://github.com/webpack-contrib/less-loader/issues/245)) ([011cc73](https://github.com/webpack-contrib/less-loader/commit/011cc73)) 330 | 331 | 332 | 333 | 334 | ## [4.0.5](https://github.com/webpack-contrib/less-loader/compare/v4.0.4...v4.0.5) (2017-07-10) 335 | 336 | 337 | ### Chore 338 | 339 | * support `webpack@3` ([670ab18](https://github.com/webpack-contrib/less-loader/commit/670ab18)) 340 | 341 | 342 | 343 | ## [4.0.4](https://github.com/webpack-contrib/less-loader/compare/v4.0.3...v4.0.4) (2017-05-30) 344 | 345 | 346 | ### Bug Fixes 347 | 348 | * resolve `[@import](https://github.com/import)` with absolute paths ([#201](https://github.com/webpack-contrib/less-loader/issues/201)) ([a3f9601](https://github.com/webpack-contrib/less-loader/commit/a3f9601)), closes [webpack-contrib/less-loader#93](https://github.com/webpack-contrib/less-loader/issues/93) 349 | 350 | 351 | 352 | 353 | ## [4.0.3](https://github.com/webpack-contrib/less-loader/compare/v4.0.2...v4.0.3) (2017-03-30) 354 | 355 | 356 | ### Bug Fixes 357 | 358 | * sourcesContent missing in source maps ([df28035](https://github.com/webpack-contrib/less-loader/commit/df28035)) 359 | 360 | 361 | 362 | 363 | ## [4.0.2](https://github.com/webpack-contrib/less-loader/compare/v4.0.1...v4.0.2) (2017-03-21) 364 | 365 | 366 | ### Bug Fixes 367 | 368 | * Plugin.install is not a function ([f8ae245](https://github.com/webpack-contrib/less-loader/commit/f8ae245)) 369 | 370 | 371 | 372 | 373 | ## [4.0.1](https://github.com/webpack-contrib/less-loader/compare/v4.0.0...v4.0.1) (2017-03-21) 374 | 375 | 376 | ### Bug Fixes 377 | 378 | * wrong entry point in package.json ([918bfe9](https://github.com/webpack-contrib/less-loader/commit/918bfe9)), closes [#161](https://github.com/webpack-contrib/less-loader/issues/161) [#179](https://github.com/webpack-contrib/less-loader/issues/179) [#177](https://github.com/webpack-contrib/less-loader/issues/177) 379 | 380 | 381 | 382 | 383 | # [4.0.0](https://github.com/webpack-contrib/less-loader/compare/v3.0.0...v4.0.0) (2017-03-20) 384 | 385 | 386 | ### Bug Fixes 387 | 388 | * error where not all files were watched ([53c90fc](https://github.com/webpack-contrib/less-loader/commit/53c90fc)) 389 | * resolve alias ([98d4e63](https://github.com/webpack-contrib/less-loader/commit/98d4e63)) 390 | 391 | 392 | ### Chores 393 | 394 | * **dependencies:** Update peer dependencies ([24a6f66](https://github.com/webpack-contrib/less-loader/commit/24a6f66)) 395 | 396 | 397 | ### Features 398 | 399 | * **source-maps:** refactor source maps handling ([895044f](https://github.com/webpack-contrib/less-loader/commit/895044f)) 400 | * allow user to choose between resolvers ([1d6e505](https://github.com/webpack-contrib/less-loader/commit/1d6e505)) 401 | * improve formatting of error messages ([39772a5](https://github.com/webpack-contrib/less-loader/commit/39772a5)) 402 | * make any file type importable ([d3022b8](https://github.com/webpack-contrib/less-loader/commit/d3022b8)) 403 | * remove root option ([39ad4f8](https://github.com/webpack-contrib/less-loader/commit/39ad4f8)) 404 | 405 | 406 | ### BREAKING CHANGES 407 | 408 | * If you've already configured your `resolve.alias` with a `.less` extension, you can now remove that wrong extension. 409 | * The root option was never documented, so it's very unlikely that this is actually a breaking change. However, since the option was removed, we need to flag this as breaking. 410 | * **dependencies:** Require webpack 2 as peer dependency 411 | * **source-maps:** Since the map is now passed as an object to the next loader, this could potentially break if another loader than the css-loader is used. The css-loader accepts both. 412 | 413 | 414 | 415 | Changelog 416 | --------- 417 | 418 | ### 3.0.0 419 | 420 | - **Breaking**: Remove node 0.10 and 0.12 support 421 | - **Breaking**: Remove official webpack 1 support. There are no breaking changes for webpack 1 with `3.0.0`, but future release won't be tested against webpack 1 422 | - **Breaking**: Remove synchronous compilation support [#152](https://github.com/webpack-contrib/less-loader/pull/152) [#84](https://github.com/webpack-contrib/less-loader/issues/84) 423 | - Reduce npm package size by using the [files](https://docs.npmjs.com/files/package.json#files) property in the `package.json` 424 | 425 | 426 | ### 2.2.3 427 | 428 | - Fix missing path information in source map [#73](https://github.com/webpack/less-loader/pull/73) 429 | - Add deprecation warning [#84](https://github.com/webpack/less-loader/issues/84) 430 | 431 | ### 2.2.2 432 | 433 | - Fix issues with synchronous less functions like `data-uri()`, `image-size()`, `image-width()`, `image-height()` [#31](https://github.com/webpack/less-loader/issues/31) [#38](https://github.com/webpack/less-loader/issues/38) [#43](https://github.com/webpack/less-loader/issues/43) [#58](https://github.com/webpack/less-loader/pull/58) 434 | 435 | ### 2.2.1 436 | 437 | - Improve Readme 438 | 439 | ### 2.2.0 440 | 441 | - Added option to specify LESS plugins [#40](https://github.com/webpack/less-loader/pull/40) 442 | -------------------------------------------------------------------------------- /test/loader.test.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | import lessPluginGlob from "less-plugin-glob"; 5 | import CustomImportPlugin from "./fixtures/folder/customImportPlugin"; 6 | 7 | import { 8 | compile, 9 | getCodeFromBundle, 10 | getCodeFromLess, 11 | getCompiler, 12 | getErrors, 13 | getWarnings, 14 | validateDependencies, 15 | } from "./helpers"; 16 | 17 | const CustomFileLoaderPlugin = require("./fixtures/folder/customFileLoaderPlugin"); 18 | 19 | const nodeModulesPath = path.resolve(__dirname, "fixtures", "node_modules"); 20 | 21 | jest.setTimeout(30000); 22 | 23 | describe("loader", () => { 24 | it("should work", async () => { 25 | const testId = "./basic.less"; 26 | const compiler = getCompiler(testId); 27 | const stats = await compile(compiler); 28 | const codeFromBundle = getCodeFromBundle(stats, compiler); 29 | const codeFromLess = await getCodeFromLess(testId); 30 | 31 | expect(codeFromBundle.css).toBe(codeFromLess.css); 32 | expect(codeFromBundle.css).toMatchSnapshot("css"); 33 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 34 | expect(getErrors(stats)).toMatchSnapshot("errors"); 35 | }); 36 | 37 | it("should compile data-uri function", async () => { 38 | const testId = "./data-uri.less"; 39 | const compiler = getCompiler(testId); 40 | const stats = await compile(compiler); 41 | const codeFromBundle = getCodeFromBundle(stats, compiler); 42 | const codeFromLess = await getCodeFromLess(testId); 43 | 44 | expect(codeFromBundle.css).toBe(codeFromLess.css); 45 | expect(codeFromBundle.css).toMatchSnapshot("css"); 46 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 47 | expect(getErrors(stats)).toMatchSnapshot("errors"); 48 | }); 49 | 50 | it("should transform urls", async () => { 51 | const testId = "./url-path.less"; 52 | const compiler = getCompiler(testId); 53 | const stats = await compile(compiler); 54 | const codeFromBundle = getCodeFromBundle(stats, compiler); 55 | const codeFromLess = await getCodeFromLess(testId); 56 | 57 | expect(codeFromBundle.css).toBe(codeFromLess.css); 58 | expect(codeFromBundle.css).toMatchSnapshot("css"); 59 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 60 | expect(getErrors(stats)).toMatchSnapshot("errors"); 61 | }); 62 | 63 | it("should install plugins", async () => { 64 | let pluginInstalled = false; 65 | // Using prototype inheritance here since Less plugins are usually instances of classes 66 | // See https://github.com/webpack/less-loader/issues/181#issuecomment-288220113 67 | const testPlugin = { 68 | install() { 69 | pluginInstalled = true; 70 | }, 71 | }; 72 | const sourceMap = { outputSourceFiles: false }; 73 | const plugins = [testPlugin]; 74 | const testId = "./basic.less"; 75 | const compiler = await getCompiler(testId, { 76 | sourceMap: true, 77 | lessOptions: { plugins, sourceMap }, 78 | }); 79 | const stats = await compile(compiler); 80 | 81 | expect(plugins).toHaveLength(1); 82 | expect(sourceMap).toEqual({ outputSourceFiles: false }); 83 | expect(pluginInstalled).toBe(true); 84 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 85 | expect(getErrors(stats)).toMatchSnapshot("errors"); 86 | }); 87 | 88 | it("should import from plugins", async () => { 89 | const testId = "./empty.less"; 90 | const compiler = getCompiler(testId, { 91 | lessOptions: { 92 | plugins: [new CustomImportPlugin()], 93 | }, 94 | }); 95 | const stats = await compile(compiler); 96 | const codeFromBundle = getCodeFromBundle(stats, compiler); 97 | const codeFromLess = await getCodeFromLess(testId, { 98 | lessOptions: { 99 | plugins: [new CustomImportPlugin()], 100 | }, 101 | }); 102 | 103 | expect(codeFromBundle.css).toBe(codeFromLess.css); 104 | expect(codeFromBundle.css).toMatchSnapshot("css"); 105 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 106 | expect(getErrors(stats)).toMatchSnapshot("errors"); 107 | }); 108 | 109 | it("should work third-party plugins as fileLoader", async () => { 110 | const testId = "./file-load.less"; 111 | const compiler = getCompiler(testId, { 112 | lessOptions: { 113 | plugins: [new CustomFileLoaderPlugin()], 114 | }, 115 | }); 116 | const stats = await compile(compiler); 117 | const codeFromBundle = getCodeFromBundle(stats, compiler); 118 | const codeFromLess = await getCodeFromLess(testId, { 119 | lessOptions: { 120 | plugins: [new CustomFileLoaderPlugin()], 121 | }, 122 | }); 123 | 124 | expect(codeFromBundle.css).toBe(codeFromLess.css); 125 | expect(codeFromBundle.css).toMatchSnapshot("css"); 126 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 127 | expect(getErrors(stats)).toMatchSnapshot("errors"); 128 | }); 129 | 130 | it("should not alter the original options object", async () => { 131 | const options = { lessOptions: { plugins: [] } }; 132 | const copiedOptions = { ...options }; 133 | 134 | const testId = "./empty.less"; 135 | const compiler = getCompiler(testId, options); 136 | const stats = await compile(compiler); 137 | 138 | expect(copiedOptions).toEqual(options); 139 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 140 | expect(getErrors(stats)).toMatchSnapshot("errors"); 141 | }); 142 | 143 | it("should resolve all imports", async () => { 144 | const testId = "./import.less"; 145 | const compiler = getCompiler(testId); 146 | const stats = await compile(compiler); 147 | const codeFromBundle = getCodeFromBundle(stats, compiler); 148 | const codeFromLess = await getCodeFromLess(testId); 149 | 150 | expect(codeFromBundle.css).toBe(codeFromLess.css); 151 | expect(codeFromBundle.css).toMatchSnapshot("css"); 152 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 153 | expect(getErrors(stats)).toMatchSnapshot("errors"); 154 | }); 155 | 156 | it("should resolve nested imports", async () => { 157 | const testId = "./import-nested.less"; 158 | const compiler = getCompiler(testId); 159 | const stats = await compile(compiler); 160 | const codeFromBundle = getCodeFromBundle(stats, compiler); 161 | const codeFromLess = await getCodeFromLess(testId); 162 | 163 | expect(codeFromBundle.css).toBe(codeFromLess.css); 164 | expect(codeFromBundle.css).toMatchSnapshot("css"); 165 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 166 | expect(getErrors(stats)).toMatchSnapshot("errors"); 167 | }); 168 | 169 | it("should work lessOptions.relativeUrls is true", async () => { 170 | const testId = "./import-relative.less"; 171 | const compiler = getCompiler(testId, { 172 | lessOptions: { 173 | relativeUrls: true, 174 | }, 175 | }); 176 | const stats = await compile(compiler); 177 | const codeFromBundle = getCodeFromBundle(stats, compiler); 178 | const codeFromLess = await getCodeFromLess(testId, { 179 | lessOptions: { 180 | relativeUrls: true, 181 | }, 182 | }); 183 | 184 | expect(codeFromBundle.css).toBe(codeFromLess.css); 185 | expect(codeFromBundle.css).toMatchSnapshot("css"); 186 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 187 | expect(getErrors(stats)).toMatchSnapshot("errors"); 188 | }); 189 | 190 | it("should work lessOptions.relativeUrls is false", async () => { 191 | const testId = "./import-relative.less"; 192 | const compiler = getCompiler(testId, { 193 | lessOptions: { 194 | relativeUrls: false, 195 | }, 196 | }); 197 | const stats = await compile(compiler); 198 | const codeFromBundle = getCodeFromBundle(stats, compiler); 199 | const codeFromLess = await getCodeFromLess(testId, { 200 | lessOptions: { 201 | relativeUrls: false, 202 | }, 203 | }); 204 | 205 | expect(codeFromBundle.css).toBe(codeFromLess.css); 206 | expect(codeFromBundle.css).toMatchSnapshot("css"); 207 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 208 | expect(getErrors(stats)).toMatchSnapshot("errors"); 209 | }); 210 | 211 | it("should resolve all imports from node_modules using webpack's resolver", async () => { 212 | const testId = "./import-webpack.less"; 213 | const compiler = getCompiler(testId); 214 | const stats = await compile(compiler); 215 | const codeFromBundle = getCodeFromBundle(stats, compiler); 216 | const codeFromLess = await getCodeFromLess(testId); 217 | 218 | expect(codeFromBundle.css).toBe(codeFromLess.css); 219 | expect(codeFromBundle.css).toMatchSnapshot("css"); 220 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 221 | expect(getErrors(stats)).toMatchSnapshot("errors"); 222 | }); 223 | 224 | it("should resolve aliases in different variants", async () => { 225 | const testId = "./import-webpack-aliases.less"; 226 | const compiler = getCompiler( 227 | testId, 228 | {}, 229 | { 230 | resolve: { 231 | alias: { 232 | fileAlias: path.resolve(__dirname, "fixtures", "img.less"), 233 | assets: path.resolve(__dirname, "fixtures"), 234 | }, 235 | }, 236 | }, 237 | ); 238 | const stats = await compile(compiler); 239 | const codeFromBundle = getCodeFromBundle(stats, compiler); 240 | const codeFromLess = await getCodeFromLess(testId); 241 | 242 | expect(codeFromBundle.css).toBe(codeFromLess.css); 243 | expect(codeFromBundle.css).toMatchSnapshot("css"); 244 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 245 | expect(getErrors(stats)).toMatchSnapshot("errors"); 246 | }); 247 | 248 | it("should resolve all imports from the given paths using Less resolver", async () => { 249 | const testId = "./import-paths.less"; 250 | const compiler = getCompiler(testId, { 251 | lessOptions: { 252 | paths: [path.resolve(nodeModulesPath, "some")], 253 | }, 254 | }); 255 | const stats = await compile(compiler); 256 | const codeFromBundle = getCodeFromBundle(stats, compiler); 257 | const codeFromLess = await getCodeFromLess(testId); 258 | 259 | expect(codeFromBundle.css).toBe(codeFromLess.css); 260 | expect(codeFromBundle.css).toMatchSnapshot("css"); 261 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 262 | expect(getErrors(stats)).toMatchSnapshot("errors"); 263 | }); 264 | 265 | it("should not to disable webpack's resolver by passing an empty paths array", async () => { 266 | const testId = "./import-webpack-aliases.less"; 267 | const compiler = getCompiler( 268 | testId, 269 | { 270 | lessOptions: { 271 | paths: [], 272 | }, 273 | }, 274 | { 275 | resolve: { 276 | alias: { 277 | fileAlias: path.resolve(__dirname, "fixtures", "img.less"), 278 | assets: path.resolve(__dirname, "fixtures"), 279 | }, 280 | }, 281 | }, 282 | ); 283 | const stats = await compile(compiler); 284 | const codeFromBundle = getCodeFromBundle(stats, compiler); 285 | const codeFromLess = await getCodeFromLess(testId); 286 | 287 | expect(codeFromBundle.css).toBe(codeFromLess.css); 288 | expect(codeFromBundle.css).toMatchSnapshot("css"); 289 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 290 | expect(getErrors(stats)).toMatchSnapshot("errors"); 291 | }); 292 | 293 | it("should prefer-relative imports correctly", async () => { 294 | const testId = "./import-prefer-relative.less"; 295 | const compiler = getCompiler( 296 | testId, 297 | {}, 298 | { 299 | resolve: { 300 | alias: { 301 | preferAlias: "prefer-relative/index.less", 302 | }, 303 | }, 304 | }, 305 | ); 306 | const stats = await compile(compiler); 307 | const codeFromBundle = getCodeFromBundle(stats, compiler); 308 | const codeFromLess = await getCodeFromLess(testId); 309 | 310 | expect(codeFromBundle.css).toBe(codeFromLess.css); 311 | expect(codeFromBundle.css).toMatchSnapshot("css"); 312 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 313 | expect(getErrors(stats)).toMatchSnapshot("errors"); 314 | }); 315 | 316 | it("should not try to resolve CSS imports with URLs", async () => { 317 | const testId = "./import-url.less"; 318 | const compiler = getCompiler(testId); 319 | const stats = await compile(compiler); 320 | const codeFromBundle = getCodeFromBundle(stats, compiler); 321 | const codeFromLess = await getCodeFromLess(testId); 322 | 323 | expect(codeFromBundle.css).toBe(codeFromLess.css); 324 | expect(codeFromBundle.css).toMatchSnapshot("css"); 325 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 326 | expect(getErrors(stats)).toMatchSnapshot("errors"); 327 | }); 328 | 329 | // eslint-disable-next-line jest/no-commented-out-tests 330 | // it('should delegate resolving (LESS) imports with URLs to "less" package', async () => { 331 | // const testId = "./import-keyword-url.less"; 332 | // const compiler = getCompiler(testId); 333 | // const stats = await compile(compiler); 334 | // const codeFromBundle = getCodeFromBundle(stats, compiler); 335 | // const codeFromLess = await getCodeFromLess(testId); 336 | // 337 | // expect(codeFromBundle.css).toBe(codeFromLess.css); 338 | // expect(codeFromBundle.css).toMatchSnapshot("css"); 339 | // expect(getWarnings(stats)).toMatchSnapshot("warnings"); 340 | // expect(getErrors(stats)).toMatchSnapshot("errors"); 341 | // }); 342 | 343 | it("should allow to import non-less files", async () => { 344 | const testId = "./import-non-less.less"; 345 | const compiler = getCompiler(testId); 346 | const stats = await compile(compiler); 347 | const codeFromBundle = getCodeFromBundle(stats, compiler); 348 | const codeFromLess = await getCodeFromLess(testId); 349 | 350 | expect(codeFromBundle.css).toBe(codeFromLess.css); 351 | expect(codeFromBundle.css).toMatchSnapshot("css"); 352 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 353 | expect(getErrors(stats)).toMatchSnapshot("errors"); 354 | }); 355 | 356 | it("should provide a useful error message if the import could not be found", async () => { 357 | const testId = "./error-import-not-existing.less"; 358 | const compiler = getCompiler(testId); 359 | const stats = await compile(compiler); 360 | 361 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 362 | expect(getErrors(stats)).toMatchSnapshot("errors"); 363 | }); 364 | 365 | it("should provide a useful error message if there was a syntax error", async () => { 366 | const testId = "./error-syntax.less"; 367 | const compiler = getCompiler(testId); 368 | const stats = await compile(compiler); 369 | 370 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 371 | expect(getErrors(stats)).toMatchSnapshot("errors"); 372 | }); 373 | 374 | it("should be able to import a file with an absolute path", async () => { 375 | const importedFilePath = path.resolve( 376 | __dirname, 377 | "fixtures", 378 | "import-absolute-target.less", 379 | ); 380 | 381 | const testId = "./import-absolute.less"; 382 | const compiler = getCompiler(testId, { 383 | lessOptions: { 384 | globalVars: { 385 | absolutePath: `'${importedFilePath}'`, 386 | }, 387 | }, 388 | }); 389 | const stats = await compile(compiler); 390 | const codeFromBundle = getCodeFromBundle(stats, compiler); 391 | 392 | expect(codeFromBundle.css).toMatchSnapshot("css"); 393 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 394 | expect(getErrors(stats)).toMatchSnapshot("errors"); 395 | }); 396 | 397 | it("should add all resolved imports as dependencies", async () => { 398 | const testId = "./import.less"; 399 | const compiler = getCompiler(testId); 400 | const stats = await compile(compiler); 401 | const { fileDependencies } = stats.compilation; 402 | 403 | validateDependencies(fileDependencies); 404 | 405 | const fixtures = [ 406 | path.resolve(__dirname, "fixtures", "import.less"), 407 | path.resolve(__dirname, "fixtures", "css.css"), 408 | path.resolve(__dirname, "fixtures", "basic.less"), 409 | ]; 410 | 411 | for (const fixture of fixtures) { 412 | expect(fileDependencies.has(fixture)).toBe(true); 413 | } 414 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 415 | expect(getErrors(stats)).toMatchSnapshot("errors"); 416 | }); 417 | 418 | it("should add all resolved imports as dependencies, including aliased ones", async () => { 419 | const testId = "./import-webpack-alias.less"; 420 | const compiler = getCompiler( 421 | testId, 422 | {}, 423 | { 424 | resolve: { 425 | alias: { 426 | "aliased-some": "some", 427 | }, 428 | }, 429 | }, 430 | ); 431 | const stats = await compile(compiler); 432 | const { fileDependencies } = stats.compilation; 433 | 434 | validateDependencies(fileDependencies); 435 | 436 | const fixtures = [ 437 | path.resolve(__dirname, "fixtures", "import-webpack-alias.less"), 438 | path.resolve( 439 | __dirname, 440 | "fixtures", 441 | "node_modules", 442 | "some", 443 | "module.less", 444 | ), 445 | ]; 446 | 447 | for (const fixture of fixtures) { 448 | expect(fileDependencies.has(fixture)).toBe(true); 449 | } 450 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 451 | expect(getErrors(stats)).toMatchSnapshot("errors"); 452 | }); 453 | 454 | it("should add all resolved imports as dependencies, including those from the Less resolver", async () => { 455 | const testId = "./import-dependency.less"; 456 | const compiler = getCompiler(testId, { 457 | lessOptions: { 458 | paths: [__dirname, nodeModulesPath], 459 | }, 460 | }); 461 | const stats = await compile(compiler); 462 | const { fileDependencies } = stats.compilation; 463 | 464 | validateDependencies(fileDependencies); 465 | 466 | const fixtures = [ 467 | path.resolve(__dirname, "fixtures", "import-dependency.less"), 468 | path.resolve( 469 | __dirname, 470 | "fixtures", 471 | "node_modules", 472 | "some", 473 | "module.less", 474 | ), 475 | ]; 476 | 477 | for (const fixture of fixtures) { 478 | expect(fileDependencies.has(fixture)).toBe(true); 479 | } 480 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 481 | expect(getErrors(stats)).toMatchSnapshot("errors"); 482 | }); 483 | 484 | it("should add a file with an error as dependency so that the watcher is triggered when the error is fixed", async () => { 485 | const testId = "./error-import-file-with-error.less"; 486 | const compiler = getCompiler(testId, { 487 | lessOptions: { 488 | paths: [__dirname, nodeModulesPath], 489 | }, 490 | }); 491 | const stats = await compile(compiler); 492 | const { fileDependencies } = stats.compilation; 493 | 494 | validateDependencies(fileDependencies); 495 | 496 | const fixtures = [ 497 | path.resolve(__dirname, "fixtures", "error-import-file-with-error.less"), 498 | path.resolve(__dirname, "fixtures", "error-syntax.less"), 499 | ]; 500 | 501 | for (const fixture of fixtures) { 502 | expect(fileDependencies.has(fixture)).toBe(true); 503 | } 504 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 505 | expect(getErrors(stats)).toMatchSnapshot("errors"); 506 | }); 507 | 508 | it("should add all resolved imports as dependencies, including node_modules", async () => { 509 | const testId = "./import-webpack.less"; 510 | const compiler = getCompiler(testId); 511 | const stats = await compile(compiler); 512 | const { fileDependencies } = stats.compilation; 513 | 514 | validateDependencies(fileDependencies); 515 | 516 | const fixtures = [ 517 | path.resolve(__dirname, "fixtures", "import-webpack.less"), 518 | path.resolve( 519 | __dirname, 520 | "fixtures", 521 | "node_modules", 522 | "some", 523 | "module.less", 524 | ), 525 | ]; 526 | 527 | for (const fixture of fixtures) { 528 | expect(fileDependencies.has(fixture)).toBe(true); 529 | } 530 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 531 | expect(getErrors(stats)).toMatchSnapshot("errors"); 532 | }); 533 | 534 | it("should watch imports correctly", async () => { 535 | const testId = "./watch.less"; 536 | const compiler = getCompiler(testId); 537 | const stats = await compile(compiler); 538 | const codeFromBundle = getCodeFromBundle(stats, compiler); 539 | const codeFromLess = await getCodeFromLess(testId); 540 | const { fileDependencies } = stats.compilation; 541 | 542 | validateDependencies(fileDependencies); 543 | 544 | const fixtures = [ 545 | path.resolve(__dirname, "fixtures", "watch.less"), 546 | path.resolve( 547 | __dirname, 548 | "fixtures", 549 | "node_modules", 550 | "package", 551 | "style.less", 552 | ), 553 | ]; 554 | 555 | for (const fixture of fixtures) { 556 | expect(fileDependencies.has(fixture)).toBe(true); 557 | } 558 | 559 | expect(codeFromBundle.css).toBe(codeFromLess.css); 560 | expect(codeFromBundle.css).toMatchSnapshot("css"); 561 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 562 | expect(getErrors(stats)).toMatchSnapshot("errors"); 563 | }); 564 | 565 | it("should get absolute path relative rootContext", async () => { 566 | const testId = "./import-absolute-2.less"; 567 | const compiler = getCompiler( 568 | testId, 569 | {}, 570 | { 571 | context: path.resolve(__dirname), 572 | entry: path.resolve(__dirname, "./fixtures", testId), 573 | }, 574 | ); 575 | const stats = await compile(compiler); 576 | 577 | const codeFromBundle = getCodeFromBundle(stats, compiler); 578 | 579 | expect(codeFromBundle.css).toMatchSnapshot("css"); 580 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 581 | expect(getErrors(stats)).toMatchSnapshot("errors"); 582 | }); 583 | 584 | it("should resolve unresolved url with alias", async () => { 585 | const testId = "./import-absolute-3.less"; 586 | const compiler = getCompiler( 587 | testId, 588 | {}, 589 | { 590 | resolve: { 591 | alias: { 592 | "/styles/style.less": path.resolve( 593 | __dirname, 594 | "fixtures", 595 | "basic.less", 596 | ), 597 | }, 598 | }, 599 | }, 600 | ); 601 | const stats = await compile(compiler); 602 | const codeFromBundle = getCodeFromBundle(stats, compiler); 603 | const codeFromLess = await getCodeFromLess(testId); 604 | 605 | expect(codeFromBundle.css).toBe(codeFromLess.css); 606 | expect(codeFromBundle.css).toMatchSnapshot("css"); 607 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 608 | expect(getErrors(stats)).toMatchSnapshot("errors"); 609 | }); 610 | 611 | it("should resolve absolute path", async () => { 612 | // Create the file with absolute path 613 | const file = path.resolve(__dirname, "fixtures", "generated-1.less"); 614 | const absolutePath = path.resolve(__dirname, "fixtures", "basic.less"); 615 | 616 | fs.writeFileSync(file, `@import "${absolutePath}";`); 617 | 618 | const testId = "./generated-1.less"; 619 | const compiler = getCompiler(testId); 620 | const stats = await compile(compiler); 621 | const codeFromBundle = getCodeFromBundle(stats, compiler); 622 | const codeFromLess = await getCodeFromLess(testId); 623 | 624 | expect(codeFromBundle.css).toBe(codeFromLess.css); 625 | expect(codeFromBundle.css).toMatchSnapshot("css"); 626 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 627 | expect(getErrors(stats)).toMatchSnapshot("errors"); 628 | }); 629 | 630 | it("should resolve absolute path with alias", async () => { 631 | // Create the file with absolute path 632 | const file = path.resolve(__dirname, "fixtures", "generated-2.less"); 633 | const absolutePath = path.resolve(__dirname, "fixtures", "unresolved.less"); 634 | 635 | fs.writeFileSync(file, `@import "${absolutePath}";`); 636 | 637 | const config = {}; 638 | config.resolve = {}; 639 | config.resolve.alias = {}; 640 | config.resolve.alias[absolutePath] = path.resolve( 641 | __dirname, 642 | "fixtures", 643 | "basic.less", 644 | ); 645 | 646 | const testId = "./generated-2.less"; 647 | const compiler = getCompiler(testId, {}, config); 648 | const stats = await compile(compiler); 649 | const codeFromBundle = getCodeFromBundle(stats, compiler); 650 | 651 | expect(codeFromBundle.css).toMatchSnapshot("css"); 652 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 653 | expect(getErrors(stats)).toMatchSnapshot("errors"); 654 | }); 655 | 656 | it("should resolve non-less import with alias", async () => { 657 | const testId = "./import-non-less-2.less"; 658 | const compiler = getCompiler( 659 | testId, 660 | {}, 661 | { 662 | resolve: { 663 | alias: { 664 | "../../some.file": path.resolve( 665 | __dirname, 666 | "fixtures", 667 | "folder", 668 | "some.file", 669 | ), 670 | }, 671 | }, 672 | }, 673 | ); 674 | const stats = await compile(compiler); 675 | const codeFromBundle = getCodeFromBundle(stats, compiler); 676 | const codeFromLess = await getCodeFromLess(testId); 677 | 678 | expect(codeFromBundle.css).toBe(codeFromLess.css); 679 | expect(codeFromBundle.css).toMatchSnapshot("css"); 680 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 681 | expect(getErrors(stats)).toMatchSnapshot("errors"); 682 | }); 683 | 684 | // eslint-disable-next-line jest/no-commented-out-tests 685 | // it("should not add to dependencies imports with URLs", async () => { 686 | // const testId = "./import-url-deps.less"; 687 | // const compiler = getCompiler(testId); 688 | // const stats = await compile(compiler); 689 | // const codeFromBundle = getCodeFromBundle(stats, compiler); 690 | // const codeFromLess = await getCodeFromLess(testId); 691 | // const { fileDependencies } = stats.compilation; 692 | // 693 | // validateDependencies(fileDependencies); 694 | // 695 | // Array.from(fileDependencies).forEach((item) => { 696 | // ["http", "https"].forEach((protocol) => { 697 | // expect(item.includes(protocol)).toBe(false); 698 | // }); 699 | // }); 700 | // 701 | // expect(codeFromBundle.css).toBe(codeFromLess.css); 702 | // expect(codeFromBundle.css).toMatchSnapshot("css"); 703 | // expect(getWarnings(stats)).toMatchSnapshot("warnings"); 704 | // expect(getErrors(stats)).toMatchSnapshot("errors"); 705 | // }); 706 | 707 | it("should add path to dependencies", async () => { 708 | // Create the file with absolute path 709 | const file = path.resolve(__dirname, "fixtures", "generated-3.less"); 710 | const absolutePath = path.resolve(__dirname, "fixtures", "basic.less"); 711 | 712 | fs.writeFileSync(file, `@import "${absolutePath}";`); 713 | 714 | const testId = "./generated-3.less"; 715 | const compiler = getCompiler(testId); 716 | const stats = await compile(compiler); 717 | const codeFromBundle = getCodeFromBundle(stats, compiler); 718 | const { fileDependencies } = stats.compilation; 719 | 720 | validateDependencies(fileDependencies); 721 | 722 | let isAddedToDependencies = false; 723 | 724 | for (const item of fileDependencies) { 725 | if (item === absolutePath) { 726 | isAddedToDependencies = true; 727 | } 728 | } 729 | 730 | expect(isAddedToDependencies).toBe(true); 731 | expect(codeFromBundle.css).toMatchSnapshot("css"); 732 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 733 | expect(getErrors(stats)).toMatchSnapshot("errors"); 734 | }); 735 | 736 | it('should resolve the "less" field from the "exports" field from "package.json"', async () => { 737 | const testId = "./import-package-with-exports.less"; 738 | const compiler = getCompiler(testId); 739 | const stats = await compile(compiler); 740 | const codeFromBundle = getCodeFromBundle(stats, compiler); 741 | const codeFromLess = await getCodeFromLess(testId); 742 | 743 | expect(codeFromBundle.css).toBe(codeFromLess.css); 744 | expect(codeFromBundle.css).toMatchSnapshot("css"); 745 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 746 | expect(getErrors(stats)).toMatchSnapshot("errors"); 747 | }); 748 | 749 | it('should resolve "@import" without "less" extension', async () => { 750 | const testId = "./import-without-extension.less"; 751 | const compiler = getCompiler(testId); 752 | const stats = await compile(compiler); 753 | const codeFromBundle = getCodeFromBundle(stats, compiler); 754 | const codeFromLess = await getCodeFromLess(testId); 755 | 756 | expect(codeFromBundle.css).toBe(codeFromLess.css); 757 | expect(codeFromBundle.css).toMatchSnapshot("css"); 758 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 759 | expect(getErrors(stats)).toMatchSnapshot("errors"); 760 | }); 761 | 762 | it('should resolve "@import" with "less" extension', async () => { 763 | const testId = "./import-without-extension.less"; 764 | const compiler = getCompiler(testId); 765 | const stats = await compile(compiler); 766 | const codeFromBundle = getCodeFromBundle(stats, compiler); 767 | const codeFromLess = await getCodeFromLess(testId); 768 | 769 | expect(codeFromBundle.css).toBe(codeFromLess.css); 770 | expect(codeFromBundle.css).toMatchSnapshot("css"); 771 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 772 | expect(getErrors(stats)).toMatchSnapshot("errors"); 773 | }); 774 | 775 | it('should resolve "@import" with "css" extension', async () => { 776 | const testId = "./import-with-css-extension.less"; 777 | const compiler = getCompiler(testId); 778 | const stats = await compile(compiler); 779 | const codeFromBundle = getCodeFromBundle(stats, compiler); 780 | const codeFromLess = await getCodeFromLess(testId); 781 | 782 | expect(codeFromBundle.css).toBe(codeFromLess.css); 783 | expect(codeFromBundle.css).toMatchSnapshot("css"); 784 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 785 | expect(getErrors(stats)).toMatchSnapshot("errors"); 786 | }); 787 | 788 | it('should resolve "@import" with "php" extension', async () => { 789 | const testId = "./import-with-php-extension.less"; 790 | const compiler = getCompiler(testId); 791 | const stats = await compile(compiler); 792 | const codeFromBundle = getCodeFromBundle(stats, compiler); 793 | const codeFromLess = await getCodeFromLess(testId); 794 | 795 | expect(codeFromBundle.css).toBe(codeFromLess.css); 796 | expect(codeFromBundle.css).toMatchSnapshot("css"); 797 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 798 | expect(getErrors(stats)).toMatchSnapshot("errors"); 799 | }); 800 | 801 | it("should work and have loaderContext in less plugins", async () => { 802 | let contextInClass = false; 803 | let contextInObject = false; 804 | 805 | class Plugin extends require("less").FileManager { 806 | constructor(less, pluginManager) { 807 | super(); 808 | 809 | if (typeof pluginManager.webpackLoaderContext !== "undefined") { 810 | contextInClass = true; 811 | } 812 | } 813 | } 814 | 815 | class CustomClassPlugin { 816 | install(less, pluginManager) { 817 | pluginManager.addFileManager(new Plugin(less, pluginManager)); 818 | } 819 | } 820 | 821 | const customObjectPlugin = { 822 | install(less, packageManager) { 823 | if (typeof packageManager.webpackLoaderContext !== "undefined") { 824 | contextInObject = true; 825 | } 826 | }, 827 | }; 828 | 829 | const testId = "./basic-plugins-2.less"; 830 | const compiler = getCompiler(testId, { 831 | lessOptions: { 832 | plugins: [new CustomClassPlugin(), customObjectPlugin], 833 | }, 834 | }); 835 | const stats = await compile(compiler); 836 | const codeFromBundle = getCodeFromBundle(stats, compiler); 837 | 838 | expect(contextInClass).toBe(true); 839 | expect(contextInObject).toBe(true); 840 | expect(codeFromBundle.css).toMatchSnapshot("css"); 841 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 842 | expect(getErrors(stats)).toMatchSnapshot("errors"); 843 | }); 844 | 845 | it("should resolve nested package", async () => { 846 | const testId = "./node_modules/less-package-2/index.less"; 847 | const compiler = getCompiler(testId); 848 | const stats = await compile(compiler); 849 | const codeFromBundle = getCodeFromBundle(stats, compiler); 850 | const codeFromLess = await getCodeFromLess(testId); 851 | 852 | expect(codeFromBundle.css).toBe(codeFromLess.css); 853 | expect(codeFromBundle.css).toMatchSnapshot("css"); 854 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 855 | expect(getErrors(stats)).toMatchSnapshot("errors"); 856 | }); 857 | 858 | it("should resolve nested package #2", async () => { 859 | const testId = "./less-package.less"; 860 | const compiler = getCompiler(testId); 861 | const stats = await compile(compiler); 862 | const codeFromBundle = getCodeFromBundle(stats, compiler); 863 | const codeFromLess = await getCodeFromLess(testId); 864 | 865 | expect(codeFromBundle.css).toBe(codeFromLess.css); 866 | expect(codeFromBundle.css).toMatchSnapshot("css"); 867 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 868 | expect(getErrors(stats)).toMatchSnapshot("errors"); 869 | }); 870 | 871 | it("should resolve in working directory", async () => { 872 | const oldCwd = process.cwd(); 873 | 874 | process.chdir(path.resolve(__dirname, "fixtures")); 875 | 876 | const testId = "./resolve-working-directory/index.less"; 877 | const compiler = getCompiler(testId); 878 | const stats = await compile(compiler); 879 | const codeFromBundle = getCodeFromBundle(stats, compiler); 880 | const codeFromLess = await getCodeFromLess(testId); 881 | 882 | expect(codeFromBundle.css).toBe(codeFromLess.css); 883 | expect(codeFromBundle.css).toMatchSnapshot("css"); 884 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 885 | expect(getErrors(stats)).toMatchSnapshot("errors"); 886 | 887 | process.chdir(oldCwd); 888 | }); 889 | 890 | it("should work and respect the 'resolve.byDependency.less' option", async () => { 891 | const testId = "./by-dependency.less"; 892 | const compiler = getCompiler( 893 | testId, 894 | {}, 895 | { 896 | resolve: { 897 | byDependency: { 898 | less: { 899 | mainFiles: ["custom"], 900 | }, 901 | }, 902 | }, 903 | }, 904 | ); 905 | const stats = await compile(compiler); 906 | const codeFromBundle = getCodeFromBundle(stats, compiler); 907 | const codeFromLess = await getCodeFromLess(testId); 908 | 909 | expect(codeFromBundle.css).toBe(codeFromLess.css); 910 | expect(codeFromBundle.css).toMatchSnapshot("css"); 911 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 912 | expect(getErrors(stats)).toMatchSnapshot("errors"); 913 | }); 914 | 915 | it("should import from glob expressions", async () => { 916 | const testId = "./glob.less"; 917 | const compiler = getCompiler(testId, { 918 | lessOptions: { 919 | plugins: [lessPluginGlob], 920 | }, 921 | }); 922 | const stats = await compile(compiler); 923 | const codeFromBundle = getCodeFromBundle(stats, compiler); 924 | const codeFromLess = await getCodeFromLess(testId, { 925 | lessOptions: { 926 | plugins: [lessPluginGlob], 927 | }, 928 | }); 929 | 930 | expect(codeFromBundle.css).toBe(codeFromLess.css); 931 | expect(codeFromBundle.css).toMatchSnapshot("css"); 932 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 933 | expect(getErrors(stats)).toMatchSnapshot("errors"); 934 | }); 935 | 936 | it("should emit an error", async () => { 937 | const testId = "./error.less"; 938 | const compiler = getCompiler(testId); 939 | const stats = await compile(compiler); 940 | 941 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 942 | expect(getErrors(stats)).toMatchSnapshot("errors"); 943 | }); 944 | 945 | it("should work and logging", async () => { 946 | const testId = "./logging.less"; 947 | const compiler = getCompiler(testId); 948 | const stats = await compile(compiler); 949 | const codeFromBundle = getCodeFromBundle(stats, compiler); 950 | const codeFromLess = await getCodeFromLess(testId); 951 | const logs = []; 952 | 953 | for (const [name, value] of stats.compilation.logging) { 954 | if (/less-loader/.test(name)) { 955 | logs.push( 956 | value.map((item) => ({ 957 | type: item.type, 958 | args: item.args, 959 | })), 960 | ); 961 | } 962 | } 963 | 964 | expect(codeFromBundle.css).toBe(codeFromLess.css); 965 | expect(codeFromBundle.css).toMatchSnapshot("css"); 966 | expect(logs).toMatchSnapshot("logs"); 967 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 968 | expect(getErrors(stats)).toMatchSnapshot("errors"); 969 | }); 970 | 971 | it('should work with a package with "sass" and "exports" fields and a custom condition (theme1)', async () => { 972 | const testId = "./import-package-with-exports-and-custom-condition.less"; 973 | const compiler = getCompiler( 974 | testId, 975 | {}, 976 | { 977 | resolve: { 978 | conditionNames: ["theme1", "..."], 979 | }, 980 | }, 981 | ); 982 | const stats = await compile(compiler); 983 | const codeFromBundle = getCodeFromBundle(stats, compiler); 984 | const codeFromLess = await getCodeFromLess( 985 | testId, 986 | {}, 987 | { 988 | packageExportsCustomConditionTestVariant: 1, 989 | }, 990 | ); 991 | 992 | expect(codeFromBundle.css).toBe(codeFromLess.css); 993 | expect(codeFromBundle.css).toMatchSnapshot("css"); 994 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 995 | expect(getErrors(stats)).toMatchSnapshot("errors"); 996 | }); 997 | 998 | it('should work with a package with "sass" and "exports" fields and a custom condition (theme2)', async () => { 999 | const testId = "./import-package-with-exports-and-custom-condition.less"; 1000 | const compiler = getCompiler( 1001 | testId, 1002 | {}, 1003 | { 1004 | resolve: { 1005 | conditionNames: ["theme2", "..."], 1006 | }, 1007 | }, 1008 | ); 1009 | const stats = await compile(compiler); 1010 | const codeFromBundle = getCodeFromBundle(stats, compiler); 1011 | const codeFromLess = await getCodeFromLess( 1012 | testId, 1013 | {}, 1014 | { 1015 | packageExportsCustomConditionTestVariant: 2, 1016 | }, 1017 | ); 1018 | 1019 | expect(codeFromBundle.css).toBe(codeFromLess.css); 1020 | expect(codeFromBundle.css).toMatchSnapshot("css"); 1021 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 1022 | expect(getErrors(stats)).toMatchSnapshot("errors"); 1023 | }); 1024 | 1025 | it("should throw an error", async () => { 1026 | const testId = "./broken.less"; 1027 | const compiler = getCompiler(testId); 1028 | const stats = await compile(compiler); 1029 | 1030 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 1031 | expect(getErrors(stats)).toMatchSnapshot("errors"); 1032 | }); 1033 | 1034 | it("should emit less warning as webpack warning", async () => { 1035 | const testId = "./warn.less"; 1036 | const compiler = getCompiler(testId, { 1037 | lessLogAsWarnOrErr: true, 1038 | }); 1039 | const stats = await compile(compiler); 1040 | const codeFromBundle = getCodeFromBundle(stats, compiler); 1041 | 1042 | expect(codeFromBundle.css).toMatchSnapshot("css"); 1043 | expect(getWarnings(stats)).toMatchSnapshot("warnings"); 1044 | expect(getErrors(stats)).toMatchSnapshot("errors"); 1045 | }); 1046 | }); 1047 | --------------------------------------------------------------------------------