Skip to content

Commit

Permalink
feat: allow to use String value for the implementation option
Browse files Browse the repository at this point in the history
  • Loading branch information
cap-Bernardito committed Jun 10, 2021
1 parent 3a66359 commit 465ffc4
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 31 deletions.
32 changes: 30 additions & 2 deletions README.md
Expand Up @@ -56,7 +56,7 @@ And run `webpack` via your preferred method.
| **[`additionalData`](#additionalData)** | `{String\|Function}` | `undefined` | Prepends/Appends `Less` code to the actual entry file. |
| **[`sourceMap`](#sourcemap)** | `{Boolean}` | `compiler.devtool` | Enables/Disables generation of source maps. |
| **[`webpackImporter`](#webpackimporter)** | `{Boolean}` | `true` | Enables/Disables the default Webpack importer. |
| **[`implementation`](#implementation)** | `{Object}` | `less` | Setup Less implementation to use. |
| **[`implementation`](#implementation)** | `{Object\|String}` | `less` | Setup Less implementation to use. |

### `lessOptions`

Expand Down Expand Up @@ -316,14 +316,16 @@ module.exports = {

### `implementation`

Type: `Object`
Type: `Object | String`

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

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

#### Object

**webpack.config.js**

```js
Expand All @@ -348,6 +350,32 @@ module.exports = {
};
```

#### String

**webpack.config.js**

```js
module.exports = {
module: {
rules: [
{
test: /\.less$/i,
use: [
"style-loader",
"css-loader",
{
loader: "less-loader",
options: {
implementation: require.resolve("less"),
},
},
],
},
],
},
};
```

## Examples

### Normal usage
Expand Down
35 changes: 26 additions & 9 deletions src/index.js
@@ -1,19 +1,36 @@
import path from "path";

import less from "less";

import schema from "./options.json";
import { getLessOptions, isUnsupportedUrl, normalizeSourceMap } from "./utils";
import {
getLessOptions,
isUnsupportedUrl,
normalizeSourceMap,
getLessImplementation,
} from "./utils";
import LessError from "./LessError";

async function lessLoader(source) {
const options = this.getOptions(schema);
const callback = this.async();
const implementation = getLessImplementation(this, options.implementation);

if (!implementation) {
callback(
new Error(`The Less implementation "${options.implementation}" not found`)
);

return;
}

const webpackContextSymbol = Symbol("loaderContext");
const lessOptions = getLessOptions(this, {
...options,
webpackContextSymbol,
});
const lessOptions = getLessOptions(
this,
{
...options,
webpackContextSymbol,
},
implementation
);
const useSourceMap =
typeof options.sourceMap === "boolean" ? options.sourceMap : this.sourceMap;

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

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

delete less[webpackContextSymbol];
delete implementation[webpackContextSymbol];

const { css, imports } = result;

Expand Down
9 changes: 8 additions & 1 deletion src/options.json
Expand Up @@ -35,7 +35,14 @@
},
"implementation": {
"description": "The implementation of the `Less` to be used (https://github.com/webpack-contrib/less-loader#implementation).",
"type": "object"
"anyOf": [
{
"type": "string"
},
{
"type": "object"
}
]
}
},
"additionalProperties": false
Expand Down
41 changes: 35 additions & 6 deletions src/utils.js
@@ -1,7 +1,6 @@
import path from "path";
import util from "util";

import less from "less";
import { klona } from "klona/full";

/* eslint-disable class-methods-use-this */
Expand Down Expand Up @@ -30,9 +29,10 @@ const MODULE_REQUEST_REGEX = /^[^?]*~/;
* Creates a Less plugin that uses webpack's resolving engine that is provided by the loaderContext.
*
* @param {LoaderContext} loaderContext
* @param {object} implementation
* @returns {LessPlugin}
*/
function createWebpackLessPlugin(loaderContext) {
function createWebpackLessPlugin(loaderContext, implementation) {
const resolve = loaderContext.getResolve({
dependencyType: "less",
conditionNames: ["less", "style"],
Expand All @@ -42,7 +42,7 @@ function createWebpackLessPlugin(loaderContext) {
preferRelative: true,
});

class WebpackFileManager extends less.FileManager {
class WebpackFileManager extends implementation.FileManager {
supports(filename) {
if (filename[0] === "/" || IS_NATIVE_WIN32_PATH.test(filename)) {
return true;
Expand Down Expand Up @@ -157,9 +157,10 @@ function createWebpackLessPlugin(loaderContext) {
*
* @param {object} loaderContext
* @param {object} loaderOptions
* @param {object} implementation
* @returns {Object}
*/
function getLessOptions(loaderContext, loaderOptions) {
function getLessOptions(loaderContext, loaderOptions, implementation) {
const options = klona(
typeof loaderOptions.lessOptions === "function"
? loaderOptions.lessOptions(loaderContext) || {}
Expand All @@ -180,7 +181,9 @@ function getLessOptions(loaderContext, loaderOptions) {
: true;

if (shouldUseWebpackImporter) {
lessOptions.plugins.unshift(createWebpackLessPlugin(loaderContext));
lessOptions.plugins.unshift(
createWebpackLessPlugin(loaderContext, implementation)
);
}

lessOptions.plugins.unshift({
Expand Down Expand Up @@ -239,4 +242,30 @@ function normalizeSourceMap(map) {
return newMap;
}

export { getLessOptions, isUnsupportedUrl, normalizeSourceMap };
function getLessImplementation(loaderContext, implementation) {
let resolvedImplementation = implementation;

if (!implementation || typeof implementation === "string") {
const lessImplPkg = implementation || "less";

try {
// eslint-disable-next-line import/no-dynamic-require, global-require
resolvedImplementation = require(lessImplPkg);
} catch (error) {
loaderContext.emitError(error);

// eslint-disable-next-line consistent-return
return;
}
}

// eslint-disable-next-line consistent-return
return resolvedImplementation;
}

export {
getLessOptions,
isUnsupportedUrl,
normalizeSourceMap,
getLessImplementation,
};
28 changes: 28 additions & 0 deletions test/__snapshots__/implementation.test.js.snap
@@ -1,5 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`"implementation" option should throw error when unresolved package: errors 1`] = `
Array [
"ModuleBuildError: Module build failed (from \`replaced original path\`):
Error: The Less implementation \\"unresolved\\" not found",
"ModuleError: Module Error (from \`replaced original path\`):
(Emitted value instead of an instance of Error) Error: Cannot find module 'unresolved' from 'src/utils.js'",
]
`;

exports[`"implementation" option should throw error when unresolved package: warnings 1`] = `Array []`;

exports[`"implementation" option should work when implementation option is string: css 1`] = `
".box {
color: #fe33ac;
border-color: #fdcdea;
background: url(box.png);
}
.box div {
-webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
"
`;

exports[`"implementation" option should work when implementation option is string: errors 1`] = `Array []`;

exports[`"implementation" option should work when implementation option is string: warnings 1`] = `Array []`;

exports[`"implementation" option should work: css 1`] = `
".box {
color: #fe33ac;
Expand Down
45 changes: 34 additions & 11 deletions test/__snapshots__/validate-options.test.js.snap
Expand Up @@ -60,25 +60,48 @@ exports[`validate options should throw an error on the "additionalData" option w
* options.additionalData should be an instance of function."
`;

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

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

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

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

exports[`validate options should throw an error on the "lessOptions" option with "[]" value 1`] = `
Expand Down
26 changes: 26 additions & 0 deletions test/implementation.test.js
Expand Up @@ -23,4 +23,30 @@ describe('"implementation" option', () => {
expect(getWarnings(stats)).toMatchSnapshot("warnings");
expect(getErrors(stats)).toMatchSnapshot("errors");
});

it("should work when implementation option is string", async () => {
const testId = "./basic.less";
const compiler = getCompiler(testId, {
implementation: require.resolve("less"),
});
const stats = await compile(compiler);
const codeFromBundle = getCodeFromBundle(stats, compiler);
const codeFromLess = await getCodeFromLess(testId);

expect(codeFromBundle.css).toBe(codeFromLess.css);
expect(codeFromBundle.css).toMatchSnapshot("css");
expect(getWarnings(stats)).toMatchSnapshot("warnings");
expect(getErrors(stats)).toMatchSnapshot("errors");
});

it("should throw error when unresolved package", async () => {
const testId = "./basic.less";
const compiler = getCompiler(testId, {
implementation: "unresolved",
});
const stats = await compile(compiler);

expect(getWarnings(stats)).toMatchSnapshot("warnings");
expect(getErrors(stats)).toMatchSnapshot("errors");
});
});
4 changes: 2 additions & 2 deletions test/validate-options.test.js
Expand Up @@ -27,8 +27,8 @@ describe("validate options", () => {
},
implementation: {
// eslint-disable-next-line global-require
success: [require("less")],
failure: [true, false, "string"],
success: [require("less"), "less"],
failure: [true, false, () => {}, []],
},
unknown: {
success: [],
Expand Down

0 comments on commit 465ffc4

Please sign in to comment.