Skip to content

Commit 465ffc4

Browse files
feat: allow to use String value for the implementation option
1 parent 3a66359 commit 465ffc4

File tree

8 files changed

+189
-31
lines changed

8 files changed

+189
-31
lines changed

README.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ And run `webpack` via your preferred method.
5656
| **[`additionalData`](#additionalData)** | `{String\|Function}` | `undefined` | Prepends/Appends `Less` code to the actual entry file. |
5757
| **[`sourceMap`](#sourcemap)** | `{Boolean}` | `compiler.devtool` | Enables/Disables generation of source maps. |
5858
| **[`webpackImporter`](#webpackimporter)** | `{Boolean}` | `true` | Enables/Disables the default Webpack importer. |
59-
| **[`implementation`](#implementation)** | `{Object}` | `less` | Setup Less implementation to use. |
59+
| **[`implementation`](#implementation)** | `{Object\|String}` | `less` | Setup Less implementation to use. |
6060

6161
### `lessOptions`
6262

@@ -316,14 +316,16 @@ module.exports = {
316316

317317
### `implementation`
318318

319-
Type: `Object`
319+
Type: `Object | String`
320320

321321
> ⚠ less-loader compatible with Less 3 and 4 versions
322322
323323
The special `implementation` option determines which implementation of Less to use. Overrides the locally installed `peerDependency` version of `less`.
324324

325325
**This option is only really useful for downstream tooling authors to ease the Less 3-to-4 transition.**
326326

327+
#### Object
328+
327329
**webpack.config.js**
328330

329331
```js
@@ -348,6 +350,32 @@ module.exports = {
348350
};
349351
```
350352

353+
#### String
354+
355+
**webpack.config.js**
356+
357+
```js
358+
module.exports = {
359+
module: {
360+
rules: [
361+
{
362+
test: /\.less$/i,
363+
use: [
364+
"style-loader",
365+
"css-loader",
366+
{
367+
loader: "less-loader",
368+
options: {
369+
implementation: require.resolve("less"),
370+
},
371+
},
372+
],
373+
},
374+
],
375+
},
376+
};
377+
```
378+
351379
## Examples
352380

353381
### Normal usage

src/index.js

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
import path from "path";
22

3-
import less from "less";
4-
53
import schema from "./options.json";
6-
import { getLessOptions, isUnsupportedUrl, normalizeSourceMap } from "./utils";
4+
import {
5+
getLessOptions,
6+
isUnsupportedUrl,
7+
normalizeSourceMap,
8+
getLessImplementation,
9+
} from "./utils";
710
import LessError from "./LessError";
811

912
async function lessLoader(source) {
1013
const options = this.getOptions(schema);
1114
const callback = this.async();
15+
const implementation = getLessImplementation(this, options.implementation);
16+
17+
if (!implementation) {
18+
callback(
19+
new Error(`The Less implementation "${options.implementation}" not found`)
20+
);
21+
22+
return;
23+
}
24+
1225
const webpackContextSymbol = Symbol("loaderContext");
13-
const lessOptions = getLessOptions(this, {
14-
...options,
15-
webpackContextSymbol,
16-
});
26+
const lessOptions = getLessOptions(
27+
this,
28+
{
29+
...options,
30+
webpackContextSymbol,
31+
},
32+
implementation
33+
);
1734
const useSourceMap =
1835
typeof options.sourceMap === "boolean" ? options.sourceMap : this.sourceMap;
1936

@@ -35,7 +52,7 @@ async function lessLoader(source) {
3552
let result;
3653

3754
try {
38-
result = await (options.implementation || less).render(data, lessOptions);
55+
result = await implementation.render(data, lessOptions);
3956
} catch (error) {
4057
if (error.filename) {
4158
// `less` returns forward slashes on windows when `webpack` resolver return an absolute windows path in `WebpackFileManager`
@@ -48,7 +65,7 @@ async function lessLoader(source) {
4865
return;
4966
}
5067

51-
delete less[webpackContextSymbol];
68+
delete implementation[webpackContextSymbol];
5269

5370
const { css, imports } = result;
5471

src/options.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@
3535
},
3636
"implementation": {
3737
"description": "The implementation of the `Less` to be used (https://github.com/webpack-contrib/less-loader#implementation).",
38-
"type": "object"
38+
"anyOf": [
39+
{
40+
"type": "string"
41+
},
42+
{
43+
"type": "object"
44+
}
45+
]
3946
}
4047
},
4148
"additionalProperties": false

src/utils.js

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import path from "path";
22
import util from "util";
33

4-
import less from "less";
54
import { klona } from "klona/full";
65

76
/* eslint-disable class-methods-use-this */
@@ -30,9 +29,10 @@ const MODULE_REQUEST_REGEX = /^[^?]*~/;
3029
* Creates a Less plugin that uses webpack's resolving engine that is provided by the loaderContext.
3130
*
3231
* @param {LoaderContext} loaderContext
32+
* @param {object} implementation
3333
* @returns {LessPlugin}
3434
*/
35-
function createWebpackLessPlugin(loaderContext) {
35+
function createWebpackLessPlugin(loaderContext, implementation) {
3636
const resolve = loaderContext.getResolve({
3737
dependencyType: "less",
3838
conditionNames: ["less", "style"],
@@ -42,7 +42,7 @@ function createWebpackLessPlugin(loaderContext) {
4242
preferRelative: true,
4343
});
4444

45-
class WebpackFileManager extends less.FileManager {
45+
class WebpackFileManager extends implementation.FileManager {
4646
supports(filename) {
4747
if (filename[0] === "/" || IS_NATIVE_WIN32_PATH.test(filename)) {
4848
return true;
@@ -157,9 +157,10 @@ function createWebpackLessPlugin(loaderContext) {
157157
*
158158
* @param {object} loaderContext
159159
* @param {object} loaderOptions
160+
* @param {object} implementation
160161
* @returns {Object}
161162
*/
162-
function getLessOptions(loaderContext, loaderOptions) {
163+
function getLessOptions(loaderContext, loaderOptions, implementation) {
163164
const options = klona(
164165
typeof loaderOptions.lessOptions === "function"
165166
? loaderOptions.lessOptions(loaderContext) || {}
@@ -180,7 +181,9 @@ function getLessOptions(loaderContext, loaderOptions) {
180181
: true;
181182

182183
if (shouldUseWebpackImporter) {
183-
lessOptions.plugins.unshift(createWebpackLessPlugin(loaderContext));
184+
lessOptions.plugins.unshift(
185+
createWebpackLessPlugin(loaderContext, implementation)
186+
);
184187
}
185188

186189
lessOptions.plugins.unshift({
@@ -239,4 +242,30 @@ function normalizeSourceMap(map) {
239242
return newMap;
240243
}
241244

242-
export { getLessOptions, isUnsupportedUrl, normalizeSourceMap };
245+
function getLessImplementation(loaderContext, implementation) {
246+
let resolvedImplementation = implementation;
247+
248+
if (!implementation || typeof implementation === "string") {
249+
const lessImplPkg = implementation || "less";
250+
251+
try {
252+
// eslint-disable-next-line import/no-dynamic-require, global-require
253+
resolvedImplementation = require(lessImplPkg);
254+
} catch (error) {
255+
loaderContext.emitError(error);
256+
257+
// eslint-disable-next-line consistent-return
258+
return;
259+
}
260+
}
261+
262+
// eslint-disable-next-line consistent-return
263+
return resolvedImplementation;
264+
}
265+
266+
export {
267+
getLessOptions,
268+
isUnsupportedUrl,
269+
normalizeSourceMap,
270+
getLessImplementation,
271+
};

test/__snapshots__/implementation.test.js.snap

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`"implementation" option should throw error when unresolved package: errors 1`] = `
4+
Array [
5+
"ModuleBuildError: Module build failed (from \`replaced original path\`):
6+
Error: The Less implementation \\"unresolved\\" not found",
7+
"ModuleError: Module Error (from \`replaced original path\`):
8+
(Emitted value instead of an instance of Error) Error: Cannot find module 'unresolved' from 'src/utils.js'",
9+
]
10+
`;
11+
12+
exports[`"implementation" option should throw error when unresolved package: warnings 1`] = `Array []`;
13+
14+
exports[`"implementation" option should work when implementation option is string: css 1`] = `
15+
".box {
16+
color: #fe33ac;
17+
border-color: #fdcdea;
18+
background: url(box.png);
19+
}
20+
.box div {
21+
-webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
22+
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
23+
}
24+
"
25+
`;
26+
27+
exports[`"implementation" option should work when implementation option is string: errors 1`] = `Array []`;
28+
29+
exports[`"implementation" option should work when implementation option is string: warnings 1`] = `Array []`;
30+
331
exports[`"implementation" option should work: css 1`] = `
432
".box {
533
color: #fe33ac;

test/__snapshots__/validate-options.test.js.snap

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,25 +60,48 @@ exports[`validate options should throw an error on the "additionalData" option w
6060
* options.additionalData should be an instance of function."
6161
`;
6262

63-
exports[`validate options should throw an error on the "implementation" option with "false" value 1`] = `
63+
exports[`validate options should throw an error on the "implementation" option with "() => {}" value 1`] = `
6464
"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema.
65-
- options.implementation should be an object:
66-
object {}
67-
-> The implementation of the \`Less\` to be used (https://github.com/webpack-contrib/less-loader#implementation)."
65+
- options.implementation should be one of these:
66+
string | object {}
67+
-> The implementation of the \`Less\` to be used (https://github.com/webpack-contrib/less-loader#implementation).
68+
Details:
69+
* options.implementation should be a string.
70+
* options.implementation should be an object:
71+
object {}"
6872
`;
6973

70-
exports[`validate options should throw an error on the "implementation" option with "string" value 1`] = `
74+
exports[`validate options should throw an error on the "implementation" option with "[]" value 1`] = `
7175
"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema.
72-
- options.implementation should be an object:
73-
object {}
74-
-> The implementation of the \`Less\` to be used (https://github.com/webpack-contrib/less-loader#implementation)."
76+
- options.implementation should be one of these:
77+
string | object {}
78+
-> The implementation of the \`Less\` to be used (https://github.com/webpack-contrib/less-loader#implementation).
79+
Details:
80+
* options.implementation should be a string.
81+
* options.implementation should be an object:
82+
object {}"
83+
`;
84+
85+
exports[`validate options should throw an error on the "implementation" option with "false" value 1`] = `
86+
"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema.
87+
- options.implementation should be one of these:
88+
string | object {}
89+
-> The implementation of the \`Less\` to be used (https://github.com/webpack-contrib/less-loader#implementation).
90+
Details:
91+
* options.implementation should be a string.
92+
* options.implementation should be an object:
93+
object {}"
7594
`;
7695

7796
exports[`validate options should throw an error on the "implementation" option with "true" value 1`] = `
7897
"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema.
79-
- options.implementation should be an object:
80-
object {}
81-
-> The implementation of the \`Less\` to be used (https://github.com/webpack-contrib/less-loader#implementation)."
98+
- options.implementation should be one of these:
99+
string | object {}
100+
-> The implementation of the \`Less\` to be used (https://github.com/webpack-contrib/less-loader#implementation).
101+
Details:
102+
* options.implementation should be a string.
103+
* options.implementation should be an object:
104+
object {}"
82105
`;
83106

84107
exports[`validate options should throw an error on the "lessOptions" option with "[]" value 1`] = `

test/implementation.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,30 @@ describe('"implementation" option', () => {
2323
expect(getWarnings(stats)).toMatchSnapshot("warnings");
2424
expect(getErrors(stats)).toMatchSnapshot("errors");
2525
});
26+
27+
it("should work when implementation option is string", async () => {
28+
const testId = "./basic.less";
29+
const compiler = getCompiler(testId, {
30+
implementation: require.resolve("less"),
31+
});
32+
const stats = await compile(compiler);
33+
const codeFromBundle = getCodeFromBundle(stats, compiler);
34+
const codeFromLess = await getCodeFromLess(testId);
35+
36+
expect(codeFromBundle.css).toBe(codeFromLess.css);
37+
expect(codeFromBundle.css).toMatchSnapshot("css");
38+
expect(getWarnings(stats)).toMatchSnapshot("warnings");
39+
expect(getErrors(stats)).toMatchSnapshot("errors");
40+
});
41+
42+
it("should throw error when unresolved package", async () => {
43+
const testId = "./basic.less";
44+
const compiler = getCompiler(testId, {
45+
implementation: "unresolved",
46+
});
47+
const stats = await compile(compiler);
48+
49+
expect(getWarnings(stats)).toMatchSnapshot("warnings");
50+
expect(getErrors(stats)).toMatchSnapshot("errors");
51+
});
2652
});

test/validate-options.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ describe("validate options", () => {
2727
},
2828
implementation: {
2929
// eslint-disable-next-line global-require
30-
success: [require("less")],
31-
failure: [true, false, "string"],
30+
success: [require("less"), "less"],
31+
failure: [true, false, () => {}, []],
3232
},
3333
unknown: {
3434
success: [],

0 commit comments

Comments
 (0)