From 393147064672ace986ec84aca21f69f0ab819a9c Mon Sep 17 00:00:00 2001 From: Alexey Lavinsky Date: Fri, 24 Apr 2020 14:18:43 +0300 Subject: [PATCH] feat: `paths` now works with webpack resolver --- README.md | 4 +- package.json | 3 +- src/getLessOptions.js | 4 +- src/index.js | 9 +- .../appendData-option.test.js.snap | 23 + test/__snapshots__/index.test.js.snap | 37 -- test/__snapshots__/loader.test.js.snap | 343 +++++++++++++ .../prependData-option.test.js.snap | 23 + .../sourceMap-options.test.js.snap | 5 + .../validate-options.test.js.snap | 182 +++++++ test/appendData-option.test.js | 39 ++ test/cjs.test.js | 8 + test/fixtures/append-data.less | 5 + test/fixtures/{less => }/basic.less | 0 test/fixtures/{less => }/css.css | 0 test/fixtures/{less => }/data-uri.less | 0 test/fixtures/{less => }/empty.less | 0 .../error-import-file-with-error.less | 0 .../{less => }/error-import-not-existing.less | 0 .../{less => }/error-mixed-resolvers.less | 0 test/fixtures/{less => }/error-syntax.less | 0 .../{less => }/folder/customImportPlugin.js | 0 test/fixtures/{less => }/folder/nested.less | 0 test/fixtures/{less => }/folder/some.file | 0 test/fixtures/{less => }/folder/url-path.less | 0 test/fixtures/{less => }/img.less | 0 .../{less => }/import-absolute-target.less | 0 test/fixtures/{less => }/import-absolute.less | 0 test/fixtures/import-dependency.less | 1 + .../{less => }/import-keyword-url.less | 0 test/fixtures/{less => }/import-nested.less | 0 test/fixtures/{less => }/import-non-less.less | 0 test/fixtures/import-paths.less | 1 + test/fixtures/{less => }/import-scope.less | 0 test/fixtures/{less => }/import-url.less | 0 .../{less => }/import-webpack-alias.less | 0 .../{less => }/import-webpack-aliases.less | 4 +- test/fixtures/{less => }/import-webpack.less | 0 test/fixtures/{less => }/import.less | 0 test/fixtures/less/append-data.less | 5 - test/fixtures/less/import-paths.less | 3 - test/fixtures/{less => }/prepend-data.less | 0 test/fixtures/{less => }/resources/circle.svg | 0 test/fixtures/{less => }/source-map.less | 0 test/fixtures/{less => }/url-path.less | 0 test/helpers/compareErrorMessage.js | 17 - test/helpers/compile.js | 56 +-- test/helpers/createSpec.js | 100 ---- test/helpers/execute.js | 19 + test/helpers/getCodeFromBundle.js | 32 ++ test/helpers/getCodeFromLess.js | 114 +++++ test/helpers/getCompiler.js | 49 ++ test/helpers/getErrors.js | 2 +- test/helpers/getWarnings.js | 5 + test/helpers/helperLoader.js | 11 - test/helpers/index.js | 23 + test/helpers/moduleRules.js | 64 --- test/helpers/normalizeErrors.js | 7 +- test/helpers/readAsset.js | 23 + test/helpers/readAssets.js | 11 + test/helpers/readFixture.js | 29 -- test/helpers/someFileLoader.js | 5 - test/helpers/testLoader.js | 11 + test/index.test.js | 449 ------------------ test/loader.test.js | 442 +++++++++++++++++ test/prependData-option.test.js | 39 ++ test/sourceMap-options.test.js | 25 + test/validate-options.test.js | 74 +++ 68 files changed, 1516 insertions(+), 790 deletions(-) create mode 100644 test/__snapshots__/appendData-option.test.js.snap delete mode 100644 test/__snapshots__/index.test.js.snap create mode 100644 test/__snapshots__/loader.test.js.snap create mode 100644 test/__snapshots__/prependData-option.test.js.snap create mode 100644 test/__snapshots__/sourceMap-options.test.js.snap create mode 100644 test/__snapshots__/validate-options.test.js.snap create mode 100644 test/appendData-option.test.js create mode 100644 test/cjs.test.js create mode 100644 test/fixtures/append-data.less rename test/fixtures/{less => }/basic.less (100%) rename test/fixtures/{less => }/css.css (100%) rename test/fixtures/{less => }/data-uri.less (100%) rename test/fixtures/{less => }/empty.less (100%) rename test/fixtures/{less => }/error-import-file-with-error.less (100%) rename test/fixtures/{less => }/error-import-not-existing.less (100%) rename test/fixtures/{less => }/error-mixed-resolvers.less (100%) rename test/fixtures/{less => }/error-syntax.less (100%) rename test/fixtures/{less => }/folder/customImportPlugin.js (100%) rename test/fixtures/{less => }/folder/nested.less (100%) rename test/fixtures/{less => }/folder/some.file (100%) rename test/fixtures/{less => }/folder/url-path.less (100%) rename test/fixtures/{less => }/img.less (100%) rename test/fixtures/{less => }/import-absolute-target.less (100%) rename test/fixtures/{less => }/import-absolute.less (100%) create mode 100644 test/fixtures/import-dependency.less rename test/fixtures/{less => }/import-keyword-url.less (100%) rename test/fixtures/{less => }/import-nested.less (100%) rename test/fixtures/{less => }/import-non-less.less (100%) create mode 100644 test/fixtures/import-paths.less rename test/fixtures/{less => }/import-scope.less (100%) rename test/fixtures/{less => }/import-url.less (100%) rename test/fixtures/{less => }/import-webpack-alias.less (100%) rename test/fixtures/{less => }/import-webpack-aliases.less (63%) rename test/fixtures/{less => }/import-webpack.less (100%) rename test/fixtures/{less => }/import.less (100%) delete mode 100644 test/fixtures/less/append-data.less delete mode 100644 test/fixtures/less/import-paths.less rename test/fixtures/{less => }/prepend-data.less (100%) rename test/fixtures/{less => }/resources/circle.svg (100%) rename test/fixtures/{less => }/source-map.less (100%) rename test/fixtures/{less => }/url-path.less (100%) delete mode 100644 test/helpers/compareErrorMessage.js delete mode 100644 test/helpers/createSpec.js create mode 100644 test/helpers/execute.js create mode 100644 test/helpers/getCodeFromBundle.js create mode 100644 test/helpers/getCodeFromLess.js create mode 100644 test/helpers/getCompiler.js create mode 100644 test/helpers/getWarnings.js delete mode 100644 test/helpers/helperLoader.js create mode 100644 test/helpers/index.js delete mode 100644 test/helpers/moduleRules.js create mode 100644 test/helpers/readAsset.js create mode 100644 test/helpers/readAssets.js delete mode 100644 test/helpers/readFixture.js delete mode 100644 test/helpers/someFileLoader.js create mode 100644 test/helpers/testLoader.js delete mode 100644 test/index.test.js create mode 100644 test/loader.test.js create mode 100644 test/prependData-option.test.js create mode 100644 test/sourceMap-options.test.js create mode 100644 test/validate-options.test.js diff --git a/README.md b/README.md index 041a09b8..556c58d1 100644 --- a/README.md +++ b/README.md @@ -411,7 +411,7 @@ module.exports = { #### Less resolver -If you specify the `paths` option, the `less-loader` will not use webpack's resolver. Modules, that can't be resolved in the local folder, will be searched in the given `paths`. This is Less' default behavior. `paths` should be an array with absolute paths: +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: **webpack.config.js** @@ -443,8 +443,6 @@ module.exports = { }; ``` -In this case, all webpack features like importing non-Less files or aliasing won't work of course. - ### Plugins In order to use [plugins](http://lesscss.org/usage/#plugins), simply set the diff --git a/package.json b/package.json index cb913270..14f1dd00 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,7 @@ "lint:prettier": "prettier --list-different .", "lint:js": "eslint --cache .", "lint": "npm-run-all -l -p \"lint:**\"", - "test:build": "node test/helpers/createSpec.js", - "test:only": "cross-env NODE_ENV=test npm run test:build && cross-env NODE_ENV=test jest", + "test:only": "cross-env NODE_ENV=test jest", "test:watch": "npm run test:only -- --watch", "test:coverage": "npm run test:only -- --collectCoverageFrom=\"src/**/*.js\" --coverage", "pretest": "npm run lint", diff --git a/src/getLessOptions.js b/src/getLessOptions.js index a788d185..c68c548c 100644 --- a/src/getLessOptions.js +++ b/src/getLessOptions.js @@ -52,9 +52,7 @@ function getLessOptions(loaderContext, loaderOptions, content) { data, }; - if (typeof lessOptions.paths === 'undefined') { - lessOptions.plugins.push(createWebpackLessPlugin(loaderContext)); - } + lessOptions.plugins.push(createWebpackLessPlugin(loaderContext)); const useSourceMap = typeof loaderOptions.sourceMap === 'boolean' diff --git a/src/index.js b/src/index.js index 8b0f87b2..16661436 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,3 @@ -import { promisify } from 'util'; - import less from 'less'; import { getOptions } from 'loader-utils'; @@ -10,10 +8,8 @@ import getLessOptions from './getLessOptions'; import removeSourceMappingUrl from './removeSourceMappingUrl'; import formatLessError from './formatLessError'; -const render = promisify(less.render.bind(less)); - function lessLoader(source) { - const options = getOptions(this) || {}; + const options = getOptions(this); validateOptions(schema, options, { name: 'Less Loader', @@ -23,7 +19,8 @@ function lessLoader(source) { const callback = this.async(); const lessOptions = getLessOptions(this, options, source); - render(lessOptions.data, lessOptions) + less + .render(lessOptions.data, lessOptions) .then(({ css, map, imports }) => { imports.forEach(this.addDependency, this); diff --git a/test/__snapshots__/appendData-option.test.js.snap b/test/__snapshots__/appendData-option.test.js.snap new file mode 100644 index 00000000..5440d02c --- /dev/null +++ b/test/__snapshots__/appendData-option.test.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`appendData option should work append data as function: css 1`] = ` +".background { + color: coral; +} +" +`; + +exports[`appendData option should work append data as function: errors 1`] = `Array []`; + +exports[`appendData option should work append data as function: warnings 1`] = `Array []`; + +exports[`appendData option should work append data as string: css 1`] = ` +".background { + color: coral; +} +" +`; + +exports[`appendData option should work append data as string: errors 1`] = `Array []`; + +exports[`appendData option should work append data as string: warnings 1`] = `Array []`; diff --git a/test/__snapshots__/index.test.js.snap b/test/__snapshots__/index.test.js.snap deleted file mode 100644 index 15c02572..00000000 --- a/test/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,37 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should fail if a file is tried to be loaded from include paths and with webpack's resolver simultaneously 1`] = ` -"Module build failed (from ./src/cjs.js): - - -@import \\"some/module.less\\"; -@import \\"~some/module.less\\"; -^ -'~some/module.less' wasn't found. Tried - /test/fixtures/less/~some/module.less,/test/fixtures/node_modules/~some/module.less,npm://~some/module.less,~some/module.less - in /test/fixtures/less/error-mixed-resolvers.less (line 3, column 0)" -`; - -exports[`should provide a useful error message if the import could not be found: errors 1`] = ` -Array [ - "ModuleBuildError: Module build failed (from \`replaced original path\`): - - -@import \\"not-existing\\"; -^ -'not-existing' wasn't found. Tried - /test/fixtures/less/not-existing.less,npm://not-existing,npm://not-existing.less,not-existing.less - in /test/fixtures/less/error-import-not-existing.less (line 1, column 0)", - "ModuleError: Module Error (from \`replaced original path\`): -Can't resolve 'not-existing.less' in '/test/fixtures/less'", -] -`; - -exports[`should provide a useful error message if there was a syntax error 1`] = ` -"Module build failed (from ./src/cjs.js): - - -but this is a syntax error - -^ -Unrecognised input. Possibly missing something - in /test/fixtures/less/error-syntax.less (line 6, column 0)" -`; diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap new file mode 100644 index 00000000..b1b1e73c --- /dev/null +++ b/test/__snapshots__/loader.test.js.snap @@ -0,0 +1,343 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`loader should add a file with an error as dependency so that the watcher is triggered when the error is fixed: errors 1`] = ` +Array [ + "ModuleBuildError: Module build failed (from \`replaced original path\`): +", +] +`; + +exports[`loader should add a file with an error as dependency so that the watcher is triggered when the error is fixed: warnings 1`] = `Array []`; + +exports[`loader should add all resolved imports as dependencies, including aliased ones: errors 1`] = `Array []`; + +exports[`loader should add all resolved imports as dependencies, including aliased ones: warnings 1`] = `Array []`; + +exports[`loader should add all resolved imports as dependencies, including node_modules: errors 1`] = `Array []`; + +exports[`loader should add all resolved imports as dependencies, including node_modules: warnings 1`] = `Array []`; + +exports[`loader should add all resolved imports as dependencies, including those from the Less resolver: errors 1`] = `Array []`; + +exports[`loader should add all resolved imports as dependencies, including those from the Less resolver: warnings 1`] = `Array []`; + +exports[`loader should add all resolved imports as dependencies: errors 1`] = `Array []`; + +exports[`loader should add all resolved imports as dependencies: warnings 1`] = `Array []`; + +exports[`loader should allow to import non-less files: css 1`] = ` +".some-file { + background: hotpink; +} +" +`; + +exports[`loader should allow to import non-less files: errors 1`] = `Array []`; + +exports[`loader should allow to import non-less files: warnings 1`] = `Array []`; + +exports[`loader should be able to import a file with an absolute path: css 1`] = ` +".it-works { + color: yellow; +} +" +`; + +exports[`loader should be able to import a file with an absolute path: errors 1`] = `Array []`; + +exports[`loader should be able to import a file with an absolute path: warnings 1`] = `Array []`; + +exports[`loader should compile data-uri function: css 1`] = ` +".img { + 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\\"); +} +" +`; + +exports[`loader should compile data-uri function: errors 1`] = `Array []`; + +exports[`loader should compile data-uri function: warnings 1`] = `Array []`; + +exports[`loader should delegate resolving (LESS) imports with URLs to "less" package: css 1`] = ` +"@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 300; + src: local('Open Sans Light'), local('OpenSans-Light'), url(https://fonts.gstatic.com/s/opensans/v17/mem5YaGs126MiZpBA-UN_r8OUuhs.ttf) format('truetype'); +} +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-weight: 400; + src: local('Open Sans Regular'), local('OpenSans-Regular'), url(https://fonts.gstatic.com/s/opensans/v17/mem8YaGs126MiZpBA-UFVZ0e.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v20/KFOlCnqEu92Fr1MmSU5fBBc9.ttf) format('truetype'); +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Mu4mxP.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v20/KFOlCnqEu92Fr1MmEU9fBBc9.ttf) format('truetype'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v20/KFOlCnqEu92Fr1MmWUlfBBc9.ttf) format('truetype'); +} +" +`; + +exports[`loader should delegate resolving (LESS) imports with URLs to "less" package: errors 1`] = `Array []`; + +exports[`loader should delegate resolving (LESS) imports with URLs to "less" package: warnings 1`] = `Array []`; + +exports[`loader should import from plugins: css 1`] = ` +".imported-class { + color: coral; +} +" +`; + +exports[`loader should import from plugins: errors 1`] = `Array []`; + +exports[`loader should import from plugins: warnings 1`] = `Array []`; + +exports[`loader should install plugins: errors 1`] = `Array []`; + +exports[`loader should install plugins: warnings 1`] = `Array []`; + +exports[`loader should not alter the original options object: errors 1`] = `Array []`; + +exports[`loader should not alter the original options object: warnings 1`] = `Array []`; + +exports[`loader should not to disable webpack's resolver by passing an empty paths array: css 1`] = ` +".img { + background: url(some/img.jpg); +} +.img2 { + background: url(../img.jpg); +} +.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); +} +body { + background: url(assets/resources/circle.svg); +} +.abs { + background: url(~assets/resources/circle.svg); +} +" +`; + +exports[`loader should not to disable webpack's resolver by passing an empty paths array: errors 1`] = `Array []`; + +exports[`loader should not to disable webpack's resolver by passing an empty paths array: warnings 1`] = `Array []`; + +exports[`loader should not try to resolve CSS imports with URLs: css 1`] = ` +"@import url(\\"http://fonts.googleapis.com/css?family=Roboto:300,400,500\\"); +@import url(\\"https://fonts.googleapis.com/css?family=Roboto:300,400,500\\"); +@import url(\\"//fonts.googleapis.com/css?family=Roboto:300,400,500\\"); +" +`; + +exports[`loader should not try to resolve CSS imports with URLs: errors 1`] = `Array []`; + +exports[`loader should not try to resolve CSS imports with URLs: warnings 1`] = `Array []`; + +exports[`loader should provide a useful error message if the import could not be found: errors 1`] = ` +Array [ + "ModuleBuildError: Module build failed (from \`replaced original path\`): +", + "ModuleError: Module Error (from \`replaced original path\`): +Can't resolve 'not-existing.less' in '/test/fixtures'", +] +`; + +exports[`loader should provide a useful error message if the import could not be found: warnings 1`] = `Array []`; + +exports[`loader should provide a useful error message if there was a syntax error: errors 1`] = ` +Array [ + "ModuleBuildError: Module build failed (from \`replaced original path\`): +", +] +`; + +exports[`loader should provide a useful error message if there was a syntax error: warnings 1`] = `Array []`; + +exports[`loader should resolve aliases in diffrent variants: css 1`] = ` +".img { + background: url(some/img.jpg); +} +.img2 { + background: url(../img.jpg); +} +.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); +} +body { + background: url(assets/resources/circle.svg); +} +.abs { + background: url(~assets/resources/circle.svg); +} +" +`; + +exports[`loader should resolve aliases in diffrent variants: errors 1`] = `Array []`; + +exports[`loader should resolve aliases in diffrent variants: warnings 1`] = `Array []`; + +exports[`loader should resolve all imports from node_modules using webpack's resolver: css 1`] = ` +"@import \\"~some/css.css\\"; +@import \\"~some/css.css\\"; +#it-works { + color: hotpink; +} +.modules-dir-some-module, +#it-works { + background: hotpink; +} +#it-works { + margin: 10px; +} +" +`; + +exports[`loader should resolve all imports from node_modules using webpack's resolver: css 2`] = ` +"@import \\"~@scope/css.css\\"; +.modules-dir-scope-module, +#it-works { + color: hotpink; +} +#it-works { + margin: 10px; +} +" +`; + +exports[`loader should resolve all imports from node_modules using webpack's resolver: errors 1`] = `Array []`; + +exports[`loader should resolve all imports from node_modules using webpack's resolver: errors 2`] = `Array []`; + +exports[`loader should resolve all imports from node_modules using webpack's resolver: warnings 1`] = `Array []`; + +exports[`loader should resolve all imports from node_modules using webpack's resolver: warnings 2`] = `Array []`; + +exports[`loader should resolve all imports from the given paths using Less resolver: css 1`] = ` +".modules-dir-some-module { + color: hotpink; +} +" +`; + +exports[`loader should resolve all imports from the given paths using Less resolver: errors 1`] = `Array []`; + +exports[`loader should resolve all imports from the given paths using Less resolver: warnings 1`] = `Array []`; + +exports[`loader should resolve all imports: css 1`] = ` +"@import \\"css.css\\"; +@import \\"css.css\\"; +.classical-css, +#it-works { + background: hotpink; +} +.box, +#it-works { + 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); +} +#it-works { + margin: 10px; +} +" +`; + +exports[`loader should resolve all imports: errors 1`] = `Array []`; + +exports[`loader should resolve all imports: warnings 1`] = `Array []`; + +exports[`loader should resolve nested imports: css 1`] = ` +".top-import { + background: red; +} +.nested-import { + background: coral; +} +" +`; + +exports[`loader should resolve nested imports: errors 1`] = `Array []`; + +exports[`loader should resolve nested imports: warnings 1`] = `Array []`; + +exports[`loader should transform urls: css 1`] = ` +".img4 { + background: url(folder/img.jpg); +} +.img5 { + background: url(folder/some/img.jpg); +} +.img6 { + background: url(./img.jpg); +} +.img1 { + background: url(img.jpg); +} +.img2 { + background: url(some/img.jpg); +} +.img3 { + background: url(../img.jpg); +} +" +`; + +exports[`loader should transform urls: errors 1`] = `Array []`; + +exports[`loader should transform urls: warnings 1`] = `Array []`; + +exports[`loader should work: 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[`loader should work: errors 1`] = `Array []`; + +exports[`loader should work: warnings 1`] = `Array []`; diff --git a/test/__snapshots__/prependData-option.test.js.snap b/test/__snapshots__/prependData-option.test.js.snap new file mode 100644 index 00000000..e83d19d9 --- /dev/null +++ b/test/__snapshots__/prependData-option.test.js.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`prependData option should work prepend data as function: css 1`] = ` +".background { + color: coral; +} +" +`; + +exports[`prependData option should work prepend data as function: errors 1`] = `Array []`; + +exports[`prependData option should work prepend data as function: warnings 1`] = `Array []`; + +exports[`prependData option should work prepend data as string: css 1`] = ` +".background { + color: coral; +} +" +`; + +exports[`prependData option should work prepend data as string: errors 1`] = `Array []`; + +exports[`prependData option should work prepend data as string: warnings 1`] = `Array []`; diff --git a/test/__snapshots__/sourceMap-options.test.js.snap b/test/__snapshots__/sourceMap-options.test.js.snap new file mode 100644 index 00000000..34716980 --- /dev/null +++ b/test/__snapshots__/sourceMap-options.test.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`sourceMap options should generate source maps with sourcesContent by default: errors 1`] = `Array []`; + +exports[`sourceMap options should generate source maps with sourcesContent by default: warnings 1`] = `Array []`; diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap new file mode 100644 index 00000000..d5e8b224 --- /dev/null +++ b/test/__snapshots__/validate-options.test.js.snap @@ -0,0 +1,182 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`validate options should throw an error on the "appendData" option with "/test/" value 1`] = ` +"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. + - options.appendData should be one of these: + string | function + -> Add \`Less\` code after the actual entry file (https://github.com/webpack-contrib/less-loader#postponeddata). + Details: + * options.appendData should be a string. + * options.appendData should be an instance of function." +`; + +exports[`validate options should throw an error on the "appendData" option with "[]" value 1`] = ` +"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. + - options.appendData should be one of these: + string | function + -> Add \`Less\` code after the actual entry file (https://github.com/webpack-contrib/less-loader#postponeddata). + Details: + * options.appendData should be a string. + * options.appendData should be an instance of function." +`; + +exports[`validate options should throw an error on the "appendData" option with "{}" value 1`] = ` +"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. + - options.appendData should be one of these: + string | function + -> Add \`Less\` code after the actual entry file (https://github.com/webpack-contrib/less-loader#postponeddata). + Details: + * options.appendData should be a string. + * options.appendData should be an instance of function." +`; + +exports[`validate options should throw an error on the "appendData" option with "1" value 1`] = ` +"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. + - options.appendData should be one of these: + string | function + -> Add \`Less\` code after the actual entry file (https://github.com/webpack-contrib/less-loader#postponeddata). + Details: + * options.appendData should be a string. + * options.appendData should be an instance of function." +`; + +exports[`validate options should throw an error on the "appendData" 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.appendData should be one of these: + string | function + -> Add \`Less\` code after the actual entry file (https://github.com/webpack-contrib/less-loader#postponeddata). + Details: + * options.appendData should be a string. + * options.appendData should be an instance of function." +`; + +exports[`validate options should throw an error on the "appendData" 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.appendData should be one of these: + string | function + -> Add \`Less\` code after the actual entry file (https://github.com/webpack-contrib/less-loader#postponeddata). + Details: + * options.appendData should be a string. + * options.appendData should be an instance of function." +`; + +exports[`validate options should throw an error on the "lessOptions" option with "[]" value 1`] = ` +"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. + - options.lessOptions should be one of these: + object { … } | function + -> Options to pass through to \`less\`. (https://github.com/webpack-contrib/less-loader#lessoptions). + Details: + * options.lessOptions should be an object: + object { … } + * options.lessOptions should be an instance of function." +`; + +exports[`validate options should throw an error on the "lessOptions" option with "1" value 1`] = ` +"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. + - options.lessOptions should be one of these: + object { … } | function + -> Options to pass through to \`less\`. (https://github.com/webpack-contrib/less-loader#lessoptions). + Details: + * options.lessOptions should be an object: + object { … } + * options.lessOptions should be an instance of function." +`; + +exports[`validate options should throw an error on the "lessOptions" 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.lessOptions should be one of these: + object { … } | function + -> Options to pass through to \`less\`. (https://github.com/webpack-contrib/less-loader#lessoptions). + Details: + * options.lessOptions should be an object: + object { … } + * options.lessOptions should be an instance of function." +`; + +exports[`validate options should throw an error on the "lessOptions" option with "test" value 1`] = ` +"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. + - options.lessOptions should be one of these: + object { … } | function + -> Options to pass through to \`less\`. (https://github.com/webpack-contrib/less-loader#lessoptions). + Details: + * options.lessOptions should be an object: + object { … } + * options.lessOptions should be an instance of function." +`; + +exports[`validate options should throw an error on the "lessOptions" 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.lessOptions should be one of these: + object { … } | function + -> Options to pass through to \`less\`. (https://github.com/webpack-contrib/less-loader#lessoptions). + Details: + * options.lessOptions should be an object: + object { … } + * options.lessOptions should be an instance of function." +`; + +exports[`validate options should throw an error on the "prependData" option with "/test/" value 1`] = ` +"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. + - options.prependData should be one of these: + string | function + -> Prepends \`Less\` code before the actual entry file (https://github.com/webpack-contrib/less-loader#prependdata). + Details: + * options.prependData should be a string. + * options.prependData should be an instance of function." +`; + +exports[`validate options should throw an error on the "prependData" option with "[]" value 1`] = ` +"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. + - options.prependData should be one of these: + string | function + -> Prepends \`Less\` code before the actual entry file (https://github.com/webpack-contrib/less-loader#prependdata). + Details: + * options.prependData should be a string. + * options.prependData should be an instance of function." +`; + +exports[`validate options should throw an error on the "prependData" option with "{}" value 1`] = ` +"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. + - options.prependData should be one of these: + string | function + -> Prepends \`Less\` code before the actual entry file (https://github.com/webpack-contrib/less-loader#prependdata). + Details: + * options.prependData should be a string. + * options.prependData should be an instance of function." +`; + +exports[`validate options should throw an error on the "prependData" option with "1" value 1`] = ` +"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. + - options.prependData should be one of these: + string | function + -> Prepends \`Less\` code before the actual entry file (https://github.com/webpack-contrib/less-loader#prependdata). + Details: + * options.prependData should be a string. + * options.prependData should be an instance of function." +`; + +exports[`validate options should throw an error on the "prependData" 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.prependData should be one of these: + string | function + -> Prepends \`Less\` code before the actual entry file (https://github.com/webpack-contrib/less-loader#prependdata). + Details: + * options.prependData should be a string. + * options.prependData should be an instance of function." +`; + +exports[`validate options should throw an error on the "prependData" 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.prependData should be one of these: + string | function + -> Prepends \`Less\` code before the actual entry file (https://github.com/webpack-contrib/less-loader#prependdata). + Details: + * options.prependData should be a string. + * options.prependData should be an instance of function." +`; + +exports[`validate options should throw an error on the "sourceMap" option with "string" value 1`] = ` +"Invalid options object. Less Loader has been initialized using an options object that does not match the API schema. + - options.sourceMap should be a boolean. + -> Enables/Disables generation of source maps (https://github.com/webpack-contrib/less-loader#sourcemap)." +`; diff --git a/test/appendData-option.test.js b/test/appendData-option.test.js new file mode 100644 index 00000000..fa869907 --- /dev/null +++ b/test/appendData-option.test.js @@ -0,0 +1,39 @@ +import { + compile, + getCodeFromBundle, + getCompiler, + getErrors, + getWarnings, +} from './helpers'; + +jest.setTimeout(30000); + +describe('appendData option', () => { + it('should work append data as function', async () => { + const testId = './append-data.less'; + const compiler = getCompiler(testId, { + appendData() { + return `@color: coral;`; + }, + }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should work append data as string', async () => { + const testId = './append-data.less'; + const compiler = getCompiler(testId, { + appendData: `@color: coral;`, + }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); +}); diff --git a/test/cjs.test.js b/test/cjs.test.js new file mode 100644 index 00000000..8aba6ba8 --- /dev/null +++ b/test/cjs.test.js @@ -0,0 +1,8 @@ +import src from '../src'; +import cjs from '../src/cjs'; + +describe('cjs', () => { + it('should exported', () => { + expect(cjs).toEqual(src); + }); +}); diff --git a/test/fixtures/append-data.less b/test/fixtures/append-data.less new file mode 100644 index 00000000..b9d80350 --- /dev/null +++ b/test/fixtures/append-data.less @@ -0,0 +1,5 @@ +@color: red; + +.background { + color: @color; +} diff --git a/test/fixtures/less/basic.less b/test/fixtures/basic.less similarity index 100% rename from test/fixtures/less/basic.less rename to test/fixtures/basic.less diff --git a/test/fixtures/less/css.css b/test/fixtures/css.css similarity index 100% rename from test/fixtures/less/css.css rename to test/fixtures/css.css diff --git a/test/fixtures/less/data-uri.less b/test/fixtures/data-uri.less similarity index 100% rename from test/fixtures/less/data-uri.less rename to test/fixtures/data-uri.less diff --git a/test/fixtures/less/empty.less b/test/fixtures/empty.less similarity index 100% rename from test/fixtures/less/empty.less rename to test/fixtures/empty.less diff --git a/test/fixtures/less/error-import-file-with-error.less b/test/fixtures/error-import-file-with-error.less similarity index 100% rename from test/fixtures/less/error-import-file-with-error.less rename to test/fixtures/error-import-file-with-error.less diff --git a/test/fixtures/less/error-import-not-existing.less b/test/fixtures/error-import-not-existing.less similarity index 100% rename from test/fixtures/less/error-import-not-existing.less rename to test/fixtures/error-import-not-existing.less diff --git a/test/fixtures/less/error-mixed-resolvers.less b/test/fixtures/error-mixed-resolvers.less similarity index 100% rename from test/fixtures/less/error-mixed-resolvers.less rename to test/fixtures/error-mixed-resolvers.less diff --git a/test/fixtures/less/error-syntax.less b/test/fixtures/error-syntax.less similarity index 100% rename from test/fixtures/less/error-syntax.less rename to test/fixtures/error-syntax.less diff --git a/test/fixtures/less/folder/customImportPlugin.js b/test/fixtures/folder/customImportPlugin.js similarity index 100% rename from test/fixtures/less/folder/customImportPlugin.js rename to test/fixtures/folder/customImportPlugin.js diff --git a/test/fixtures/less/folder/nested.less b/test/fixtures/folder/nested.less similarity index 100% rename from test/fixtures/less/folder/nested.less rename to test/fixtures/folder/nested.less diff --git a/test/fixtures/less/folder/some.file b/test/fixtures/folder/some.file similarity index 100% rename from test/fixtures/less/folder/some.file rename to test/fixtures/folder/some.file diff --git a/test/fixtures/less/folder/url-path.less b/test/fixtures/folder/url-path.less similarity index 100% rename from test/fixtures/less/folder/url-path.less rename to test/fixtures/folder/url-path.less diff --git a/test/fixtures/less/img.less b/test/fixtures/img.less similarity index 100% rename from test/fixtures/less/img.less rename to test/fixtures/img.less diff --git a/test/fixtures/less/import-absolute-target.less b/test/fixtures/import-absolute-target.less similarity index 100% rename from test/fixtures/less/import-absolute-target.less rename to test/fixtures/import-absolute-target.less diff --git a/test/fixtures/less/import-absolute.less b/test/fixtures/import-absolute.less similarity index 100% rename from test/fixtures/less/import-absolute.less rename to test/fixtures/import-absolute.less diff --git a/test/fixtures/import-dependency.less b/test/fixtures/import-dependency.less new file mode 100644 index 00000000..ec2dc4fb --- /dev/null +++ b/test/fixtures/import-dependency.less @@ -0,0 +1 @@ +@import "some/module.less"; diff --git a/test/fixtures/less/import-keyword-url.less b/test/fixtures/import-keyword-url.less similarity index 100% rename from test/fixtures/less/import-keyword-url.less rename to test/fixtures/import-keyword-url.less diff --git a/test/fixtures/less/import-nested.less b/test/fixtures/import-nested.less similarity index 100% rename from test/fixtures/less/import-nested.less rename to test/fixtures/import-nested.less diff --git a/test/fixtures/less/import-non-less.less b/test/fixtures/import-non-less.less similarity index 100% rename from test/fixtures/less/import-non-less.less rename to test/fixtures/import-non-less.less diff --git a/test/fixtures/import-paths.less b/test/fixtures/import-paths.less new file mode 100644 index 00000000..520d319b --- /dev/null +++ b/test/fixtures/import-paths.less @@ -0,0 +1 @@ +@import "module.less"; diff --git a/test/fixtures/less/import-scope.less b/test/fixtures/import-scope.less similarity index 100% rename from test/fixtures/less/import-scope.less rename to test/fixtures/import-scope.less diff --git a/test/fixtures/less/import-url.less b/test/fixtures/import-url.less similarity index 100% rename from test/fixtures/less/import-url.less rename to test/fixtures/import-url.less diff --git a/test/fixtures/less/import-webpack-alias.less b/test/fixtures/import-webpack-alias.less similarity index 100% rename from test/fixtures/less/import-webpack-alias.less rename to test/fixtures/import-webpack-alias.less diff --git a/test/fixtures/less/import-webpack-aliases.less b/test/fixtures/import-webpack-aliases.less similarity index 63% rename from test/fixtures/less/import-webpack-aliases.less rename to test/fixtures/import-webpack-aliases.less index 0703da01..e1015ba1 100644 --- a/test/fixtures/less/import-webpack-aliases.less +++ b/test/fixtures/import-webpack-aliases.less @@ -1,6 +1,6 @@ @import "~fileAlias"; -@import "~assets/folder/url-path.less"; -@import "assets/folder/url-path.less"; +@import "~assets/basic.less"; +@import "assets/basic.less"; body { background: url(assets/resources/circle.svg); diff --git a/test/fixtures/less/import-webpack.less b/test/fixtures/import-webpack.less similarity index 100% rename from test/fixtures/less/import-webpack.less rename to test/fixtures/import-webpack.less diff --git a/test/fixtures/less/import.less b/test/fixtures/import.less similarity index 100% rename from test/fixtures/less/import.less rename to test/fixtures/import.less diff --git a/test/fixtures/less/append-data.less b/test/fixtures/less/append-data.less deleted file mode 100644 index 03712cc3..00000000 --- a/test/fixtures/less/append-data.less +++ /dev/null @@ -1,5 +0,0 @@ -@color1: red; - -.background { - color: @color1; -} diff --git a/test/fixtures/less/import-paths.less b/test/fixtures/less/import-paths.less deleted file mode 100644 index 63ad5132..00000000 --- a/test/fixtures/less/import-paths.less +++ /dev/null @@ -1,3 +0,0 @@ -// some/module.less is intended to be loaded from node_modules if the folder is configured as include path. -// webpack would expect this import to be prepended with a ~ character. -@import "some/module.less"; diff --git a/test/fixtures/less/prepend-data.less b/test/fixtures/prepend-data.less similarity index 100% rename from test/fixtures/less/prepend-data.less rename to test/fixtures/prepend-data.less diff --git a/test/fixtures/less/resources/circle.svg b/test/fixtures/resources/circle.svg similarity index 100% rename from test/fixtures/less/resources/circle.svg rename to test/fixtures/resources/circle.svg diff --git a/test/fixtures/less/source-map.less b/test/fixtures/source-map.less similarity index 100% rename from test/fixtures/less/source-map.less rename to test/fixtures/source-map.less diff --git a/test/fixtures/less/url-path.less b/test/fixtures/url-path.less similarity index 100% rename from test/fixtures/less/url-path.less rename to test/fixtures/url-path.less diff --git a/test/helpers/compareErrorMessage.js b/test/helpers/compareErrorMessage.js deleted file mode 100644 index 82ea7bb0..00000000 --- a/test/helpers/compareErrorMessage.js +++ /dev/null @@ -1,17 +0,0 @@ -const path = require('path'); - -const projectPath = path.resolve(__dirname, '..', '..').replace(/\\/g, '/'); -const projectPathPattern = new RegExp(projectPath, 'g'); -const CR = /\r/g; - -// We need to remove all environment dependent features -function compareErrorMessage(msg) { - const envIndependentMsg = msg - .replace(/\\/g, '/') - .replace(CR, '') - .replace(projectPathPattern, ''); - - expect(envIndependentMsg).toMatchSnapshot(); -} - -module.exports = compareErrorMessage; diff --git a/test/helpers/compile.js b/test/helpers/compile.js index 5973361c..066873ab 100644 --- a/test/helpers/compile.js +++ b/test/helpers/compile.js @@ -1,51 +1,11 @@ -const path = require('path'); - -const webpack = require('webpack'); - -const fixturePath = path.resolve(__dirname, '..', 'fixtures'); -const outputPath = path.resolve(__dirname, '..', 'output'); - -function compile(fixture, moduleRules, resolveAlias = {}) { +export default (compiler) => { return new Promise((resolve, reject) => { - const entry = path.resolve(fixturePath, 'less', `${fixture}.less`); - - webpack( - { - mode: 'development', - entry, - output: { - path: outputPath, - // omitting the js extension to prevent jest's watcher from triggering - filename: 'bundle', - }, - module: { - rules: moduleRules, - }, - resolve: { - alias: resolveAlias, - }, - }, - (err, stats) => { - const problem = - err || stats.compilation.errors[0] || stats.compilation.warnings[0]; - - if (problem) { - const message = - typeof problem === 'string' ? problem : 'Unexpected error'; - const error = new Error(problem.message || message); - - error.originalError = problem; - error.stats = stats; - - reject(error); - - return; - } - - resolve(stats); + compiler.run((error, stats) => { + if (error) { + return reject(error); } - ); - }); -} -module.exports = compile; + return resolve(stats); + }); + }); +}; diff --git a/test/helpers/createSpec.js b/test/helpers/createSpec.js deleted file mode 100644 index 9afa6c2d..00000000 --- a/test/helpers/createSpec.js +++ /dev/null @@ -1,100 +0,0 @@ -const { exec } = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -const removeSourceMappingUrl = require('../../src/removeSourceMappingUrl'); - -const projectPath = path.resolve(__dirname, '..', '..'); -const fixturesPath = path.resolve(projectPath, 'test', 'fixtures'); -const lessFixturesPath = path.resolve(fixturesPath, 'less'); -const cssFixturesPath = path.resolve(fixturesPath, 'css'); -const lessBin = require.resolve('.bin/lessc'); -const ignore = [ - 'import-non-less', - 'error-import-not-existing', - 'error-mixed-resolvers', - 'error-syntax', - 'error-import-file-with-error', - 'import-absolute', - 'import-absolute-target', - 'prepend-data', -]; -const lessReplacements = [ - [/~some\//g, '../node_modules/some/'], - [/~@scope\//g, '../node_modules/@scope/'], - [/(~)?assets\//g, '../less/'], - [/~fileAlias/g, '../less/img.less'], - [/~(aliased-)?some"/g, '../node_modules/some/module.less"'], -]; -const cssReplacements = [ - [/\.\.\/node_modules\/some\//g, '~some/'], - [/\.\.\/node_modules\/@scope\//g, '~@scope/'], -]; -// Maps test ids on cli arguments -const lessOptions = { - 'source-map': [ - '--source-map', - `--source-map-basepath=${projectPath}`, - `--source-map-rootpath=${projectPath}`, - '--source-map-less-inline', - ], - 'import-paths': [ - `--include-path=${path.resolve(fixturesPath, 'node_modules')}`, - ], -}; -const testIds = fs - .readdirSync(lessFixturesPath) - .filter( - (name) => - path.extname(name) === '.less' && - ignore.indexOf(path.basename(name, '.less')) === -1 - ) - .map((name) => path.basename(name, '.less')); - -function replace(content, replacements) { - return replacements.reduce( - (intermediate, [pattern, replacement]) => - intermediate.replace(pattern, replacement), - content - ); -} - -testIds.forEach((testId) => { - const lessFile = path.resolve(lessFixturesPath, `${testId}.less`); - const cssFile = path.resolve(cssFixturesPath, `${testId}.css`); - const originalLessContent = fs.readFileSync(lessFile, 'utf8'); - const replacedLessContent = replace(originalLessContent, lessReplacements); - - // It's safer to change the file and write it back to disk instead of piping it to the Less process - // because Less tends to create broken paths in url() statements and source maps when the content is read from stdin - // See also https://github.com/less/less.js/issues/3038 - fs.writeFileSync(lessFile, replacedLessContent, 'utf8'); - - exec( - [ - lessBin, - '--rewrite-urls', - ...(lessOptions[testId] || ''), - lessFile, - cssFile, - ].join(' '), - { cwd: projectPath }, - (err, stdout, stderr) => { - if (err || stdout || stderr) { - throw err || new Error(stdout || stderr); - } - - // We remove the source mapping url because the less-loader will do it also. - // See removeSourceMappingUrl.js for the reasoning behind this. - const cssContent = removeSourceMappingUrl( - replace(fs.readFileSync(cssFile, 'utf8'), cssReplacements) - ); - - fs.writeFileSync(lessFile, originalLessContent, 'utf8'); - fs.writeFileSync(cssFile, cssContent, 'utf8'); - } - ); - - // eslint-disable-next-line no-console - console.log(`${testId}.less -> ${cssFile}`); -}); diff --git a/test/helpers/execute.js b/test/helpers/execute.js new file mode 100644 index 00000000..4cb101db --- /dev/null +++ b/test/helpers/execute.js @@ -0,0 +1,19 @@ +import Module from 'module'; +import path from 'path'; + +const parentModule = module; + +export default (code) => { + const resource = 'test.js'; + const module = new Module(resource, parentModule); + // eslint-disable-next-line no-underscore-dangle + module.paths = Module._nodeModulePaths( + path.resolve(__dirname, '../fixtures') + ); + module.filename = resource; + + // eslint-disable-next-line no-underscore-dangle + module._compile(code, resource); + + return module.exports; +}; diff --git a/test/helpers/getCodeFromBundle.js b/test/helpers/getCodeFromBundle.js new file mode 100644 index 00000000..30443795 --- /dev/null +++ b/test/helpers/getCodeFromBundle.js @@ -0,0 +1,32 @@ +import vm from 'vm'; + +import readAsset from './readAsset'; + +function getCodeFromBundle(stats, compiler, asset) { + let code = null; + + if ( + stats && + stats.compilation && + stats.compilation.assets && + stats.compilation.assets[asset || 'main.bundle.js'] + ) { + code = readAsset(asset || 'main.bundle.js', compiler, stats); + } + + if (!code) { + throw new Error("Can't find compiled code"); + } + + const result = vm.runInNewContext( + `${code};\nmodule.exports = lessLoaderExport;`, + { + module: {}, + } + ); + + // eslint-disable-next-line no-underscore-dangle + return result.__esModule ? result.default : result; +} + +export default getCodeFromBundle; diff --git a/test/helpers/getCodeFromLess.js b/test/helpers/getCodeFromLess.js new file mode 100644 index 00000000..971be21c --- /dev/null +++ b/test/helpers/getCodeFromLess.js @@ -0,0 +1,114 @@ +import path from 'path'; +import fs from 'fs'; + +import less from 'less'; + +const pathMap = { + '~some/css.css': path.resolve( + __dirname, + '..', + 'fixtures', + 'node_modules', + 'some', + 'css.css' + ), + '~some/module': path.resolve( + __dirname, + '..', + 'fixtures', + 'node_modules', + 'some', + 'module.less' + ), + 'some/module.less': path.resolve( + __dirname, + '..', + 'fixtures', + 'node_modules', + 'some', + 'module.less' + ), + 'module.less': path.resolve( + __dirname, + '..', + 'fixtures', + 'node_modules', + 'some', + 'module.less' + ), + '~@scope/css.css': path.resolve( + __dirname, + '..', + 'fixtures', + 'node_modules', + '@scope', + 'css.css' + ), + '~@scope/module': path.resolve( + __dirname, + '..', + 'fixtures', + 'node_modules', + '@scope', + 'module.less' + ), + '~fileAlias': path.resolve(__dirname, '..', 'fixtures', 'img.less'), + fileAlias: path.resolve(__dirname, '..', 'fixtures', 'img.less'), + '~assets/basic.less': path.resolve(__dirname, '..', 'fixtures', 'basic.less'), + 'assets/basic.less': path.resolve(__dirname, '..', 'fixtures', 'basic.less'), + '@{absolutePath}': path.resolve( + __dirname, + '..', + 'fixtures', + 'import-absolute-target.less' + ), +}; + +class ResolvePlugin extends less.FileManager { + supports(filename) { + if (this.isPathAbsolute(filename)) { + return false; + } + + return true; + } + + // eslint-disable-next-line class-methods-use-this + supportsSync() { + return false; + } + + async loadFile(filename, ...args) { + const result = + pathMap[filename] || path.resolve(__dirname, '..', 'fixtures', filename); + + return super.loadFile(result, ...args); + } +} + +class CustomImportPlugin { + // eslint-disable-next-line class-methods-use-this + install(lessInstance, pluginManager) { + pluginManager.addFileManager(new ResolvePlugin()); + } +} + +async function getCodeFromLess(testId, options = {}) { + const pathToFile = path.resolve(__dirname, '..', 'fixtures', testId); + const defaultOptions = { + plugins: [new CustomImportPlugin()], + relativeUrls: true, + filename: pathToFile, + }; + const lessOptions = options.lessOptions || {}; + const data = await fs.promises.readFile(pathToFile); + + const result = await less.render(data.toString(), { + ...defaultOptions, + ...lessOptions, + }); + + return result; +} + +export default getCodeFromLess; diff --git a/test/helpers/getCompiler.js b/test/helpers/getCompiler.js new file mode 100644 index 00000000..2ca328b7 --- /dev/null +++ b/test/helpers/getCompiler.js @@ -0,0 +1,49 @@ +import path from 'path'; + +import webpack from 'webpack'; +import { createFsFromVolume, Volume } from 'memfs'; + +export default (fixture, loaderOptions = {}, config = {}) => { + const fullConfig = { + mode: 'development', + devtool: config.devtool || false, + context: path.resolve(__dirname, '../fixtures'), + entry: path.resolve(__dirname, '../fixtures', fixture), + output: { + path: path.resolve(__dirname, '../outputs'), + filename: '[name].bundle.js', + chunkFilename: '[name].chunk.js', + library: 'lessLoaderExport', + }, + module: { + rules: [ + { + test: /\.less$/i, + rules: [ + { + loader: require.resolve('./testLoader'), + }, + { + loader: path.resolve(__dirname, '../../src'), + options: loaderOptions || {}, + }, + ], + }, + ], + }, + plugins: [], + ...config, + }; + + const compiler = webpack(fullConfig); + + if (!config.outputFileSystem) { + const outputFileSystem = createFsFromVolume(new Volume()); + // Todo remove when we drop webpack@4 support + outputFileSystem.join = path.join.bind(path); + + compiler.outputFileSystem = outputFileSystem; + } + + return compiler; +}; diff --git a/test/helpers/getErrors.js b/test/helpers/getErrors.js index 085bae5b..71d940bf 100644 --- a/test/helpers/getErrors.js +++ b/test/helpers/getErrors.js @@ -1,5 +1,5 @@ import normalizeErrors from './normalizeErrors'; export default (stats) => { - return normalizeErrors(stats.compilation.errors).sort(); + return normalizeErrors(stats.compilation.errors.sort()); }; diff --git a/test/helpers/getWarnings.js b/test/helpers/getWarnings.js new file mode 100644 index 00000000..f5e5ab15 --- /dev/null +++ b/test/helpers/getWarnings.js @@ -0,0 +1,5 @@ +import normalizeErrors from './normalizeErrors'; + +export default (stats) => { + return normalizeErrors(stats.compilation.warnings.sort()); +}; diff --git a/test/helpers/helperLoader.js b/test/helpers/helperLoader.js deleted file mode 100644 index 58f3b3a3..00000000 --- a/test/helpers/helperLoader.js +++ /dev/null @@ -1,11 +0,0 @@ -function helperLoader() { - // Discard the contents - return ''; -} - -helperLoader.pitch = function pitch() { - // Extend loader context with mocks - Object.assign(this, this.query); -}; - -module.exports = helperLoader; diff --git a/test/helpers/index.js b/test/helpers/index.js new file mode 100644 index 00000000..8a1a5fee --- /dev/null +++ b/test/helpers/index.js @@ -0,0 +1,23 @@ +import compile from './compile'; +import execute from './execute'; +import getCodeFromBundle from './getCodeFromBundle'; +import getCodeFromLess from './getCodeFromLess'; +import getCompiler from './getCompiler'; +import getErrors from './getErrors'; +import getWarnings from './getWarnings'; +import normalizeErrors from './normalizeErrors'; +import readAsset from './readAsset'; +import readsAssets from './readAssets'; + +export { + compile, + execute, + getCodeFromBundle, + getCodeFromLess, + getCompiler, + getErrors, + getWarnings, + normalizeErrors, + readAsset, + readsAssets, +}; diff --git a/test/helpers/moduleRules.js b/test/helpers/moduleRules.js deleted file mode 100644 index c7529b13..00000000 --- a/test/helpers/moduleRules.js +++ /dev/null @@ -1,64 +0,0 @@ -const lessLoader = require.resolve('../../src/cjs'); -const helperLoader = require.resolve('./helperLoader.js'); -const someFileLoader = require.resolve('./someFileLoader.js'); - -function basic( - lessLoaderOptions, - lessLoaderContext = {}, - inspectCallback = () => {} -) { - return [ - { - test: /\.less$/, - use: [ - { - loader: helperLoader, - options: lessLoaderContext, - }, - { - loader: 'inspect-loader', - options: { - callback: inspectCallback, - }, - }, - { - loader: lessLoader, - options: lessLoaderOptions, - }, - ], - }, - ]; -} - -function nonLessImport(inspectCallback) { - return [ - { - test: /\.less$/, - use: [ - { - loader: helperLoader, - }, - { - loader: 'inspect-loader', - options: { - callback: inspectCallback, - }, - }, - { - loader: lessLoader, - }, - ], - }, - { - test: /some\.file$/, - use: [ - { - loader: someFileLoader, - }, - ], - }, - ]; -} - -exports.basic = basic; -exports.nonLessImport = nonLessImport; diff --git a/test/helpers/normalizeErrors.js b/test/helpers/normalizeErrors.js index 36020766..1b41f3fb 100644 --- a/test/helpers/normalizeErrors.js +++ b/test/helpers/normalizeErrors.js @@ -1,6 +1,3 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import stripAnsi from 'strip-ansi'; - function removeCWD(str) { const isWin = process.platform === 'win32'; let cwd = process.cwd(); @@ -12,13 +9,13 @@ function removeCWD(str) { cwd = cwd.replace(/\\/g, '/'); } - return stripAnsi(str) + return str .replace(/\(from .*?\)/, '(from `replaced original path`)') .replace(new RegExp(cwd, 'g'), ''); } export default (errors) => { return errors.map((error) => - removeCWD(error.toString().split('\n').slice(0, 12).join('\n')) + removeCWD(error.toString().split('\n').slice(0, 2).join('\n')) ); }; diff --git a/test/helpers/readAsset.js b/test/helpers/readAsset.js new file mode 100644 index 00000000..8f4699f0 --- /dev/null +++ b/test/helpers/readAsset.js @@ -0,0 +1,23 @@ +import path from 'path'; + +export default (asset, compiler, stats) => { + const usedFs = compiler.outputFileSystem; + const outputPath = stats.compilation.outputOptions.path; + + let data = ''; + let targetFile = asset; + + const queryStringIdx = targetFile.indexOf('?'); + + if (queryStringIdx >= 0) { + targetFile = targetFile.substr(0, queryStringIdx); + } + + try { + data = usedFs.readFileSync(path.join(outputPath, targetFile)).toString(); + } catch (error) { + data = error.toString(); + } + + return data; +}; diff --git a/test/helpers/readAssets.js b/test/helpers/readAssets.js new file mode 100644 index 00000000..a2fb7837 --- /dev/null +++ b/test/helpers/readAssets.js @@ -0,0 +1,11 @@ +import readAsset from './readAsset'; + +export default function readAssets(compiler, stats) { + const assets = {}; + + Object.keys(stats.compilation.assets).forEach((asset) => { + assets[asset] = readAsset(asset, compiler, stats); + }); + + return assets; +} diff --git a/test/helpers/readFixture.js b/test/helpers/readFixture.js deleted file mode 100644 index b4084342..00000000 --- a/test/helpers/readFixture.js +++ /dev/null @@ -1,29 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const util = require('util'); - -const readFile = util.promisify(fs.readFile); -const testFolder = path.resolve(__dirname, '..'); - -function readCssFixture(testId) { - return readFile( - path.resolve(testFolder, 'fixtures', 'css', `${testId}.css`), - 'utf8' - ); -} - -function readSourceMap(testId) { - return ( - readFile( - path.resolve(testFolder, 'fixtures', 'css', `${testId}.css.map`), - 'utf8' - ) - .then(JSON.parse) - // The map that is generated by our loader does not have a file property because the - // output file is unknown when the Less API is used. That's why we need to remove that from our fixture. - .then(({ file, ...map }) => map) - ); // eslint-disable-line no-unused-vars -} - -exports.readCssFixture = readCssFixture; -exports.readSourceMap = readSourceMap; diff --git a/test/helpers/someFileLoader.js b/test/helpers/someFileLoader.js deleted file mode 100644 index d57bd8fc..00000000 --- a/test/helpers/someFileLoader.js +++ /dev/null @@ -1,5 +0,0 @@ -function someFileLoader() { - return '.some-file { background: hotpink; }'; -} - -module.exports = someFileLoader; diff --git a/test/helpers/testLoader.js b/test/helpers/testLoader.js new file mode 100644 index 00000000..d2a696cb --- /dev/null +++ b/test/helpers/testLoader.js @@ -0,0 +1,11 @@ +function testLoader(content, sourceMap) { + const result = { css: content }; + + if (sourceMap) { + result.map = sourceMap; + } + + return `export default ${JSON.stringify(result)}`; +} + +module.exports = testLoader; diff --git a/test/index.test.js b/test/index.test.js deleted file mode 100644 index 3a6d211b..00000000 --- a/test/index.test.js +++ /dev/null @@ -1,449 +0,0 @@ -import path from 'path'; - -import compile from './helpers/compile'; -import moduleRules from './helpers/moduleRules'; -import { readCssFixture, readSourceMap } from './helpers/readFixture'; -import compareErrorMessage from './helpers/compareErrorMessage'; -import getErrors from './helpers/getErrors'; -import CustomImportPlugin from './fixtures/less/folder/customImportPlugin'; - -const nodeModulesPath = path.resolve(__dirname, 'fixtures', 'node_modules'); - -async function compileAndCompare( - fixture, - { lessLoaderOptions, lessLoaderContext, resolveAlias } = {} -) { - let inspect; - const rules = moduleRules.basic(lessLoaderOptions, lessLoaderContext, (i) => { - inspect = i; - }); - const [expectedCss] = await Promise.all([ - readCssFixture(fixture), - compile(fixture, rules, resolveAlias), - ]); - const [actualCss] = inspect.arguments; - - expect(actualCss).toBe(expectedCss); -} - -function lessFixturePath(fixture) { - return path.resolve(__dirname, 'fixtures', 'less', fixture); -} - -test('should compile simple less without errors', async () => { - await compileAndCompare('basic'); -}); - -test('should resolve all imports', async () => { - await compileAndCompare('import'); -}); - -test('should resolve nested imports', async () => { - await compileAndCompare('import-nested'); -}); - -test('should fail when passed incorrect configuration', async () => { - await expect( - compileAndCompare('basic', { - lessLoaderOptions: { randomProperty: 'randomValue' }, - }) - ).rejects.toThrow(); -}); - -test('should add all resolved imports as dependencies', async () => { - const dependencies = []; - - await compileAndCompare('import', { - lessLoaderContext: { - addDependency(dep) { - if (dependencies.indexOf(dep) === -1) { - dependencies.push(dep); - } - }, - }, - }); - - expect(dependencies).toContain(lessFixturePath('import.less')); - expect(dependencies).toContain(lessFixturePath('css.css')); - expect(dependencies).toContain(lessFixturePath('basic.less')); -}); - -test("should resolve all imports from node_modules using webpack's resolver", async () => { - await compileAndCompare('import-webpack'); -}); - -test("should resolve all imports from node_modules using webpack's resolver", async () => { - await compileAndCompare('import-scope'); -}); - -test('should add all resolved imports as dependencies, including node_modules', async () => { - const dependencies = []; - - await compileAndCompare('import-webpack', { - lessLoaderContext: { - addDependency(dep) { - if (dependencies.indexOf(dep) === -1) { - dependencies.push(dep); - } - }, - }, - }); - - expect(dependencies).toContain(lessFixturePath('import-webpack.less')); - expect(dependencies).toContain( - lessFixturePath('../node_modules/some/module.less') - ); - expect(dependencies).toContain( - lessFixturePath('../node_modules/some/css.css') - ); -}); - -test('should resolve aliases in diffrent variants', async () => { - await compileAndCompare('import-webpack-alias', { - resolveAlias: { - 'aliased-some': 'some', - fileAlias: path.resolve(__dirname, 'fixtures', 'less', 'img.less'), - assets: path.resolve(__dirname, 'fixtures', 'less'), - }, - }); -}); - -test('should add all resolved imports as dependencies, including aliased ones', async () => { - const dependencies = []; - - await compileAndCompare('import-webpack-alias', { - resolveAlias: { - 'aliased-some': 'some', - }, - lessLoaderContext: { - addDependency(dep) { - if (dependencies.indexOf(dep) === -1) { - dependencies.push(dep); - } - }, - }, - }); - - expect(dependencies).toContain(lessFixturePath('import-webpack-alias.less')); - expect(dependencies).toContain( - lessFixturePath('../node_modules/some/module.less') - ); -}); - -test("should resolve all imports from the given paths using Less' resolver", async () => { - await compileAndCompare('import-paths', { - lessLoaderOptions: { - lessOptions: { - paths: [__dirname, nodeModulesPath], - }, - }, - }); -}); - -test('should prepend data', async () => { - const loaderOptions = { - prependData() { - return `@background: red;`; - }, - }; - - let inspect; - - const rules = moduleRules.basic(loaderOptions, {}, (i) => { - inspect = i; - }); - - await compile('prepend-data', rules).catch((e) => e); - - const [css] = inspect.arguments; - - expect(css).toEqual('.background {\n color: red;\n}\n'); -}); - -test('should prepend data', async () => { - const loaderOptions = { - prependData: `@background: red;`, - }; - - let inspect; - - const rules = moduleRules.basic(loaderOptions, {}, (i) => { - inspect = i; - }); - - await compile('prepend-data', rules).catch((e) => e); - - const [css] = inspect.arguments; - - expect(css).toEqual('.background {\n color: red;\n}\n'); -}); - -test('should append data', async () => { - const loaderOptions = { - appendData() { - return `@color1: coral;`; - }, - }; - - let inspect; - - const rules = moduleRules.basic(loaderOptions, {}, (i) => { - inspect = i; - }); - - await compile('append-data', rules).catch((e) => e); - - const [css] = inspect.arguments; - - expect(css).toEqual('.background {\n color: coral;\n}\n'); -}); - -test('should append data', async () => { - const loaderOptions = { - appendData: `@color1: coral;`, - }; - - let inspect; - - const rules = moduleRules.basic(loaderOptions, {}, (i) => { - inspect = i; - }); - - await compile('append-data', rules).catch((e) => e); - - const [css] = inspect.arguments; - - expect(css).toEqual('.background {\n color: coral;\n}\n'); -}); - -test('should allow a function to be passed through for `lessOptions`', async () => { - await compileAndCompare('import-paths', { - lessLoaderOptions: { - lessOptions: () => ({ - paths: [__dirname, nodeModulesPath], - }), - }, - }); -}); - -test('should add all resolved imports as dependencies, including those from the Less resolver', async () => { - const dependencies = []; - - await compileAndCompare('import-paths', { - lessLoaderOptions: { - lessOptions: { - paths: [__dirname, nodeModulesPath], - }, - }, - lessLoaderContext: { - addDependency(dep) { - if (dependencies.indexOf(dep) === -1) { - dependencies.push(dep); - } - }, - }, - }); - - expect(dependencies).toContain(lessFixturePath('import-paths.less')); - expect(dependencies).toContain( - lessFixturePath('../node_modules/some/module.less') - ); -}); - -test("should allow to disable webpack's resolver by passing an empty paths array", async () => { - const err = await compile( - 'import-webpack', - moduleRules.basic({ lessOptions: { paths: [] } }) - ).catch((e) => e); - - expect(err).toBeInstanceOf(Error); - expect(err.message).toMatch(/'~some\/css\.css' wasn't found/); -}); - -// This test was invalid because it tested imports like this: -// @import url("//fonts.googleapis.com/css?family=Roboto:300,400,500"); -// and expected them to be preserved in the output of Less and not resolved. -// See https://github.com/less/less.js/pull/2955 -// Because of a bug in Less <3.0, imports with the substring '/css' indeed were -// parsed as CSS imports, i.e. were preserved. Now that the bug has been fixed, -// if you need to preserve an import that doesn't have the `.css` extension, -// use the `(css)` import option: -// @import (css) url("//fonts.googleapis.com/css?family=Roboto:300,400,500"); -// That's how the Less language is designed to work. -test('should not try to resolve CSS imports with URLs', async () => { - await compileAndCompare('import-url'); -}); - -test('should delegate resolving (LESS) imports with URLs to "less" package', async () => { - await compileAndCompare('import-keyword-url'); -}); - -test('should allow to import non-less files', async () => { - let inspect; - const rules = moduleRules.nonLessImport((i) => { - inspect = i; - }); - - await compile('import-non-less', rules); - - const [css] = inspect.arguments; - - expect(css).toMatch(/\.some-file {\s*background: hotpink;\s*}\s*/); -}); - -test('should compile data-uri function', async () => { - await compileAndCompare('data-uri'); -}); - -test('should transform urls', async () => { - await compileAndCompare('url-path'); -}); - -test('should generate source maps with sourcesContent by default', async () => { - let inspect; - - const rules = moduleRules.basic({ sourceMap: true }, {}, (i) => { - inspect = i; - }); - - const [expectedCss, expectedMap] = await Promise.all([ - readCssFixture('source-map'), - readSourceMap('source-map'), - compile('source-map', rules), - ]); - const [actualCss, actualMap] = inspect.arguments; - - expect(Array.isArray(actualMap.sourcesContent)).toBe(true); - expect(actualMap.sourcesContent.length).toBe(2); - - // We can't actually compare the sourcesContent because it's slightly different because of our import rewriting - delete actualMap.sourcesContent; - delete expectedMap.sourcesContent; - - expect(actualCss).toEqual(expectedCss); - expect(actualMap).toEqual(expectedMap); -}); - -test('should install plugins', async () => { - let pluginInstalled = false; - // Using prototype inheritance here since Less plugins are usually instances of classes - // See https://github.com/webpack-contrib/less-loader/issues/181#issuecomment-288220113 - const testPlugin = Object.create({ - install() { - pluginInstalled = true; - }, - }); - - await compile( - 'basic', - moduleRules.basic({ lessOptions: { plugins: [testPlugin] } }) - ); - - expect(pluginInstalled).toBe(true); -}); - -test('should import from plugins', async () => { - const loaderOptions = { - lessOptions: { - plugins: [new CustomImportPlugin()], - }, - }; - - let inspect; - - const rules = moduleRules.basic(loaderOptions, {}, (i) => { - inspect = i; - }); - - await compile('empty', rules).catch((e) => e); - - const [css] = inspect.arguments; - - expect(css).toEqual('.imported-class {\n color: coral;\n}\n'); -}); - -test('should not alter the original options object', async () => { - const options = { lessOptions: { plugins: [] } }; - const copiedOptions = { ...options }; - - await compile('basic', moduleRules.basic(options)); - - expect(copiedOptions).toEqual(options); -}); - -test("should fail if a file is tried to be loaded from include paths and with webpack's resolver simultaneously", async () => { - const err = await compile( - 'error-mixed-resolvers', - moduleRules.basic({ lessOptions: { paths: [nodeModulesPath] } }) - ).catch((e) => e); - - expect(err).toBeInstanceOf(Error); - - compareErrorMessage(err.message); -}); - -test('should provide a useful error message if the import could not be found', async () => { - const err = await compile( - 'error-import-not-existing', - moduleRules.basic() - ).catch((e) => e); - - expect(getErrors(err.stats)).toMatchSnapshot('errors'); -}); - -test('should provide a useful error message if there was a syntax error', async () => { - const err = await compile('error-syntax', moduleRules.basic()).catch( - (e) => e - ); - - expect(err).toBeInstanceOf(Error); - - compareErrorMessage(err.message); -}); - -test('should add a file with an error as dependency so that the watcher is triggered when the error is fixed', async () => { - const dependencies = []; - const lessLoaderContext = { - addDependency(dep) { - if (dependencies.indexOf(dep) === -1) { - dependencies.push(dep); - } - }, - }; - const rules = moduleRules.basic({}, lessLoaderContext); - - await compile('error-import-file-with-error', rules).catch((e) => e); - - expect(dependencies).toContain( - lessFixturePath('error-import-file-with-error.less') - ); - expect(dependencies).toContain(lessFixturePath('error-syntax.less')); -}); - -test('should be able to import a file with an absolute path', async () => { - const importedFilePath = path.resolve( - __dirname, - 'fixtures', - 'less', - 'import-absolute-target.less' - ); - const loaderOptions = { - lessOptions: { - globalVars: { - absolutePath: `'${importedFilePath}'`, - }, - }, - }; - - let inspect; - - const rules = moduleRules.basic(loaderOptions, {}, (i) => { - inspect = i; - }); - - await compile('import-absolute', rules).catch((e) => e); - - const [css] = inspect.arguments; - - expect(css).toEqual('.it-works {\n color: yellow;\n}\n'); -}); diff --git a/test/loader.test.js b/test/loader.test.js new file mode 100644 index 00000000..97bc7c17 --- /dev/null +++ b/test/loader.test.js @@ -0,0 +1,442 @@ +import path from 'path'; + +import CustomImportPlugin from './fixtures/folder/customImportPlugin'; + +import { + compile, + getCodeFromBundle, + getCodeFromLess, + getCompiler, + getErrors, + getWarnings, +} from './helpers'; + +const nodeModulesPath = path.resolve(__dirname, 'fixtures', 'node_modules'); + +jest.setTimeout(30000); + +describe('loader', () => { + it('should work', async () => { + const testId = './basic.less'; + const compiler = getCompiler(testId); + 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 compile data-uri function', async () => { + const testId = './data-uri.less'; + const compiler = getCompiler(testId); + 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 transform urls', async () => { + const testId = './url-path.less'; + const compiler = getCompiler(testId); + 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 install plugins', async () => { + let pluginInstalled = false; + // Using prototype inheritance here since Less plugins are usually instances of classes + // See https://github.com/webpack-contrib/less-loader/issues/181#issuecomment-288220113 + const testPlugin = Object.create({ + install() { + pluginInstalled = true; + }, + }); + + const testId = './basic.less'; + const compiler = await getCompiler(testId, { + lessOptions: { + plugins: [testPlugin], + }, + }); + const stats = await compile(compiler); + + expect(pluginInstalled).toBe(true); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should import from plugins', async () => { + const testId = './empty.less'; + const compiler = getCompiler(testId, { + lessOptions: { + plugins: [new CustomImportPlugin()], + }, + }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const codeFromLess = await getCodeFromLess(testId, { + lessOptions: { + plugins: [new CustomImportPlugin()], + }, + }); + + expect(codeFromBundle.css).toBe(codeFromLess.css); + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should not alter the original options object', async () => { + const options = { lessOptions: { plugins: [] } }; + const copiedOptions = { ...options }; + + const testId = './empty.less'; + const compiler = getCompiler(testId, options); + const stats = await compile(compiler); + + expect(copiedOptions).toEqual(options); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should resolve all imports', async () => { + const testId = './import.less'; + const compiler = getCompiler(testId); + 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 resolve nested imports', async () => { + const testId = './import-nested.less'; + const compiler = getCompiler(testId); + 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 resolve all imports from node_modules using webpack's resolver", async () => { + const testId = './import-webpack.less'; + const compiler = getCompiler(testId); + 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 resolve all imports from node_modules using webpack's resolver", async () => { + const testId = './import-scope.less'; + const compiler = getCompiler(testId); + 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 resolve aliases in diffrent variants', async () => { + const testId = './import-webpack-aliases.less'; + const compiler = getCompiler( + testId, + {}, + { + resolve: { + alias: { + fileAlias: path.resolve(__dirname, 'fixtures', 'img.less'), + assets: path.resolve(__dirname, 'fixtures'), + }, + }, + } + ); + 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 resolve all imports from the given paths using Less resolver', async () => { + const testId = './import-paths.less'; + const compiler = getCompiler(testId, { + lessOptions: { + paths: [path.resolve(nodeModulesPath, 'some')], + }, + }); + 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 not to disable webpack's resolver by passing an empty paths array", async () => { + const testId = './import-webpack-aliases.less'; + const compiler = getCompiler( + testId, + { + lessOptions: { + paths: [], + }, + }, + { + resolve: { + alias: { + fileAlias: path.resolve(__dirname, 'fixtures', 'img.less'), + assets: path.resolve(__dirname, 'fixtures'), + }, + }, + } + ); + 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 not try to resolve CSS imports with URLs', async () => { + const testId = './import-url.less'; + const compiler = getCompiler(testId); + 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 delegate resolving (LESS) imports with URLs to "less" package', async () => { + const testId = './import-keyword-url.less'; + const compiler = getCompiler(testId); + 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 allow to import non-less files', async () => { + const testId = './import-non-less.less'; + const compiler = getCompiler(testId); + 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 provide a useful error message if the import could not be found', async () => { + const testId = './error-import-not-existing.less'; + const compiler = getCompiler(testId); + const stats = await compile(compiler); + + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should provide a useful error message if there was a syntax error', async () => { + const testId = './error-syntax.less'; + const compiler = getCompiler(testId); + const stats = await compile(compiler); + + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should be able to import a file with an absolute path', async () => { + const importedFilePath = path.resolve( + __dirname, + 'fixtures', + 'import-absolute-target.less' + ); + + const testId = './import-absolute.less'; + const compiler = getCompiler(testId, { + lessOptions: { + globalVars: { + absolutePath: `'${importedFilePath}'`, + }, + }, + }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should add all resolved imports as dependencies', async () => { + const testId = './import.less'; + const compiler = getCompiler(testId); + const stats = await compile(compiler); + const dependencies = stats.compilation.fileDependencies; + + const fixtures = [ + path.resolve(__dirname, 'fixtures', 'import.less'), + path.resolve(__dirname, 'fixtures', 'css.css'), + path.resolve(__dirname, 'fixtures', 'basic.less'), + ]; + + fixtures.forEach((fixture) => { + expect(dependencies.has(fixture)).toBe(true); + }); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should add all resolved imports as dependencies, including aliased ones', async () => { + const testId = './import-webpack-alias.less'; + const compiler = getCompiler( + testId, + {}, + { + resolve: { + alias: { + 'aliased-some': 'some', + }, + }, + } + ); + const stats = await compile(compiler); + const dependencies = stats.compilation.fileDependencies; + + const fixtures = [ + path.resolve(__dirname, 'fixtures', 'import-webpack-alias.less'), + path.resolve( + __dirname, + 'fixtures', + 'node_modules', + 'some', + 'module.less' + ), + ]; + + fixtures.forEach((fixture) => { + expect(dependencies.has(fixture)).toBe(true); + }); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should add all resolved imports as dependencies, including those from the Less resolver', async () => { + const testId = './import-dependency.less'; + const compiler = getCompiler(testId, { + lessOptions: { + paths: [__dirname, nodeModulesPath], + }, + }); + const stats = await compile(compiler); + const dependencies = stats.compilation.fileDependencies; + + const fixtures = [ + path.resolve(__dirname, 'fixtures', 'import-dependency.less'), + path.resolve( + __dirname, + 'fixtures', + 'node_modules', + 'some', + 'module.less' + ), + ]; + + fixtures.forEach((fixture) => { + expect(dependencies.has(fixture)).toBe(true); + }); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should add a file with an error as dependency so that the watcher is triggered when the error is fixed', async () => { + const testId = './error-import-file-with-error.less'; + const compiler = getCompiler(testId, { + lessOptions: { + paths: [__dirname, nodeModulesPath], + }, + }); + const stats = await compile(compiler); + const dependencies = stats.compilation.fileDependencies; + + const fixtures = [ + path.resolve(__dirname, 'fixtures', 'error-import-file-with-error.less'), + path.resolve(__dirname, 'fixtures', 'error-syntax.less'), + ]; + + fixtures.forEach((fixture) => { + expect(dependencies.has(fixture)).toBe(true); + }); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should add all resolved imports as dependencies, including node_modules', async () => { + const testId = './import-webpack.less'; + const compiler = getCompiler(testId); + const stats = await compile(compiler); + const dependencies = stats.compilation.fileDependencies; + + const fixtures = [ + path.resolve(__dirname, 'fixtures', 'import-webpack.less'), + path.resolve( + __dirname, + 'fixtures', + 'node_modules', + 'some', + 'module.less' + ), + ]; + + fixtures.forEach((fixture) => { + expect(dependencies.has(fixture)).toBe(true); + }); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); +}); diff --git a/test/prependData-option.test.js b/test/prependData-option.test.js new file mode 100644 index 00000000..bf33eb9d --- /dev/null +++ b/test/prependData-option.test.js @@ -0,0 +1,39 @@ +import { + compile, + getCodeFromBundle, + getCompiler, + getErrors, + getWarnings, +} from './helpers'; + +jest.setTimeout(30000); + +describe('prependData option', () => { + it('should work prepend data as function', async () => { + const testId = './prepend-data.less'; + const compiler = getCompiler(testId, { + prependData() { + return `@background: coral;`; + }, + }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should work prepend data as string', async () => { + const testId = './prepend-data.less'; + const compiler = getCompiler(testId, { + prependData: `@background: coral;`, + }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); +}); diff --git a/test/sourceMap-options.test.js b/test/sourceMap-options.test.js new file mode 100644 index 00000000..66175994 --- /dev/null +++ b/test/sourceMap-options.test.js @@ -0,0 +1,25 @@ +import { + compile, + getCodeFromBundle, + getCompiler, + getErrors, + getWarnings, +} from './helpers'; + +jest.setTimeout(30000); + +describe('sourceMap options', () => { + it('should generate source maps with sourcesContent by default', async () => { + const testId = './source-map.less'; + const compiler = getCompiler(testId, { + sourceMap: true, + }); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + + expect(codeFromBundle.css).toBeDefined(); + expect(codeFromBundle.map).toBeDefined(); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); +}); diff --git a/test/validate-options.test.js b/test/validate-options.test.js new file mode 100644 index 00000000..fda31e6f --- /dev/null +++ b/test/validate-options.test.js @@ -0,0 +1,74 @@ +import { getCompiler, compile } from './helpers/index'; + +describe('validate options', () => { + const tests = { + lessOptions: { + success: [ + { strictMath: true }, + () => ({ + strictMath: true, + }), + ], + failure: [1, true, false, 'test', []], + }, + prependData: { + success: ['@background: coral;', () => '@background: coral;'], + failure: [1, true, false, /test/, [], {}], + }, + appendData: { + success: ['@background: coral;', () => '@background: coral;'], + failure: [1, true, false, /test/, [], {}], + }, + sourceMap: { + success: [true, false], + failure: ['string'], + }, + }; + + function stringifyValue(value) { + if ( + Array.isArray(value) || + (value && typeof value === 'object' && value.constructor === Object) + ) { + return JSON.stringify(value); + } + + return value; + } + + async function createTestCase(key, value, type) { + it(`should ${ + type === 'success' ? 'successfully validate' : 'throw an error on' + } the "${key}" option with "${stringifyValue(value)}" value`, async () => { + const compiler = getCompiler('./basic.less', { + [key]: value, + }); + let stats; + + try { + stats = await compile(compiler); + } finally { + if (type === 'success') { + expect(stats.hasErrors()).toBe(false); + } else if (type === 'failure') { + const { + compilation: { errors }, + } = stats; + + expect(errors).toHaveLength(1); + expect(() => { + throw new Error(errors[0].error.message); + }).toThrowErrorMatchingSnapshot(); + } + } + }); + } + + for (const [key, values] of Object.entries(tests)) { + for (const type of Object.keys(values)) { + for (const value of values[type]) { + createTestCase(key, value, type); + } + } + } +});