diff --git a/.gitignore b/.gitignore index cd5fe0e1..c83f4498 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ yarn-debug.log* /test/fixtures/css /test/fixtures/generated-1.less /test/fixtures/generated-2.less +/test/fixtures/generated-3.less /test/output .DS_Store Thumbs.db diff --git a/src/index.js b/src/index.js index 51cefc4e..dfa1a2de 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,11 @@ import { getOptions } from 'loader-utils'; import validateOptions from 'schema-utils'; import schema from './options.json'; -import { getLessOptions, getLessImplementation } from './utils'; +import { + getLessOptions, + getLessImplementation, + isUnsupportedUrl, +} from './utils'; import LessError from './LessError'; function lessLoader(source) { @@ -38,6 +42,10 @@ function lessLoader(source) { .render(data, lessOptions) .then(({ css, map, imports }) => { imports.forEach((item) => { + if (isUnsupportedUrl(item)) { + return; + } + // `less` return forward slashes on windows when `webpack` resolver return an absolute windows path in `WebpackFileManager` // Ref: https://github.com/webpack-contrib/less-loader/issues/357 this.addDependency(path.normalize(item)); diff --git a/src/utils.js b/src/utils.js index 542c4b87..100fea0f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,3 +1,5 @@ +import path from 'path'; + import less from 'less'; import clone from 'clone'; @@ -126,7 +128,7 @@ function createWebpackLessPlugin(loaderContext) { return super.loadFile(result, ...args); } - loaderContext.addDependency(result.filename); + loaderContext.addDependency(path.normalize(result.filename)); return result; } @@ -188,4 +190,15 @@ function getLessImplementation(implementation) { return less; } -export { getLessImplementation, getLessOptions }; +function isUnsupportedUrl(url) { + // Is Windows paths `c:\` + if (/^[a-zA-Z]:\\/.test(url)) { + return false; + } + + // Scheme: https://tools.ietf.org/html/rfc3986#section-3.1 + // Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3 + return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url); +} + +export { getLessImplementation, getLessOptions, isUnsupportedUrl }; diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index 40a7917c..3c55bdfc 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -25,6 +25,23 @@ exports[`loader should add all resolved imports as dependencies: errors 1`] = `A exports[`loader should add all resolved imports as dependencies: warnings 1`] = `Array []`; +exports[`loader should add path to dependencies: 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 add path to dependencies: errors 1`] = `Array []`; + +exports[`loader should add path to dependencies: warnings 1`] = `Array []`; + exports[`loader should allow to import non-less files: css 1`] = ` ".some-file { background: hotpink; @@ -135,6 +152,38 @@ exports[`loader should install plugins: errors 1`] = `Array []`; exports[`loader should install plugins: warnings 1`] = `Array []`; +exports[`loader should not add to dependencies imports with URLs: css 1`] = ` +"@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 300; + src: local('Roboto Light'), local('Roboto-Light'), url(http://fonts.gstatic.com/s/roboto/v20/KFOlCnqEu92Fr1MmSU5fBBc9.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'); +} +" +`; + +exports[`loader should not add to dependencies imports with URLs: errors 1`] = `Array []`; + +exports[`loader should not add to dependencies imports with URLs: 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 []`; diff --git a/test/fixtures/import-url-deps.less b/test/fixtures/import-url-deps.less new file mode 100644 index 00000000..33f28113 --- /dev/null +++ b/test/fixtures/import-url-deps.less @@ -0,0 +1,2 @@ +@import url("http://fonts.googleapis.com/css?family=Roboto:300"); +@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500"); diff --git a/test/loader.test.js b/test/loader.test.js index bd614b13..43fadeb2 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -616,4 +616,55 @@ describe('loader', () => { expect(getWarnings(stats)).toMatchSnapshot('warnings'); expect(getErrors(stats)).toMatchSnapshot('errors'); }); + + it('should not add to dependencies imports with URLs', async () => { + const testId = './import-url-deps.less'; + const compiler = getCompiler(testId); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const codeFromLess = await getCodeFromLess(testId); + const { fileDependencies } = stats.compilation; + + validateDependencies(fileDependencies); + + Array.from(fileDependencies).forEach((item) => { + ['http', 'https'].forEach((protocol) => { + expect(item.includes(protocol)).toBe(false); + }); + }); + + expect(codeFromBundle.css).toBe(codeFromLess.css); + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); + + it('should add path to dependencies', async () => { + // Create the file with absolute path + const file = path.resolve(__dirname, 'fixtures', 'generated-3.less'); + const absolutePath = path.resolve(__dirname, 'fixtures', 'basic.less'); + + fs.writeFileSync(file, `@import "${absolutePath}";`); + + const testId = './generated-3.less'; + const compiler = getCompiler(testId); + const stats = await compile(compiler); + const codeFromBundle = getCodeFromBundle(stats, compiler); + const { fileDependencies } = stats.compilation; + + validateDependencies(fileDependencies); + + let isAddedToDependensies = false; + + Array.from(fileDependencies).forEach((item) => { + if (item === absolutePath) { + isAddedToDependensies = true; + } + }); + + expect(isAddedToDependensies).toBe(true); + expect(codeFromBundle.css).toMatchSnapshot('css'); + expect(getWarnings(stats)).toMatchSnapshot('warnings'); + expect(getErrors(stats)).toMatchSnapshot('errors'); + }); });