diff --git a/src/evaluator.js b/src/evaluator.js index ffa674c..0f4f4d3 100644 --- a/src/evaluator.js +++ b/src/evaluator.js @@ -189,9 +189,13 @@ export default async function createEvaluator(code, options, loaderContext) { return class CustomEvaluator extends Evaluator { visitImport(imported) { + this.return += 1; + const node = this.visit(imported.path).first; const nodePath = node.string; + this.return -= 1; + if (node.name !== 'url' && nodePath && !URL_RE.test(nodePath)) { const resolved = deps.get(nodePath); diff --git a/src/index.js b/src/index.js index e713aea..f1a24d5 100644 --- a/src/index.js +++ b/src/index.js @@ -54,10 +54,12 @@ export default async function stylusLoader(source) { } } - if (typeof stylusOptions.resolveUrl !== 'undefined') { - stylusOptions.resolveUrl = {}; - - styl.define('url', resolver(stylusOptions)); + if (stylusOptions.resolveUrl !== false) { + stylusOptions.resolveUrl = { + ...stylusOptions.resolveUrl, + paths: options.paths, + }; + styl.define('url', resolver(stylusOptions.resolveUrl)); } if (typeof stylusOptions.define !== 'undefined') { diff --git a/src/lib/resolver.js b/src/lib/resolver.js index 99e72df..4c6ea87 100644 --- a/src/lib/resolver.js +++ b/src/lib/resolver.js @@ -8,36 +8,55 @@ export default function resolver(options = {}) { // eslint-disable-next-line no-underscore-dangle const _paths = options.paths || []; - function url(imported) { + function resolve(url) { const paths = _paths.concat(this.paths); - const { filename } = this; + const { filename } = url; // Compile the url - const compiler = new Compiler(imported); + const compiler = new Compiler(url); compiler.isURL = true; - const query = imported.nodes + const query = url.nodes .map((node) => { return compiler.visit(node); }) .join(''); - const components = query.split(/!/g).map((urlSegment) => { - if (!urlSegment) { - return urlSegment; - } + const components = query.split(/!/g); + const resolvedFilePath = resolveUrl(components.pop(), this.filename); + function resolveUrl(urlSegment, name) { const parsedUrl = parse(urlSegment); + const literal = new nodes.Literal(parsedUrl.href); + + // Absolute or hash + if ( + parsedUrl.protocol || + !parsedUrl.pathname || + parsedUrl.pathname[0] === '/' + ) { + return literal; + } + if (parsedUrl.protocol) { return parsedUrl.href; } - // Lookup - const found = utils.lookup(parsedUrl.pathname, paths, '', true); + let found; + + // Check that file exists + if (!options.noCheck) { + found = utils.lookup(parsedUrl.pathname, paths, '', true); + + if (!found) { + return parsedUrl.href; + } + } + if (!found) { - return parsedUrl.href; + found = parsedUrl.href; } let tail = ''; @@ -49,19 +68,26 @@ export default function resolver(options = {}) { tail += parsedUrl.hash; } - let res = path.relative(path.dirname(filename), found) + tail; + let res = + path.relative( + path.dirname(name), + options.noCheck ? path.join(path.dirname(filename), found) : found + ) + tail; if (path.sep === '\\') { res = res.replace(/\\/g, '/'); } return res; - }); + } + + components.push(resolvedFilePath); return new nodes.Literal(`url("${components.join('!')}")`); } - url.raw = true; + resolve.options = options; + resolve.raw = true; - return url; + return resolve; } diff --git a/src/utils.js b/src/utils.js index a53de5c..6ffae45 100644 --- a/src/utils.js +++ b/src/utils.js @@ -11,6 +11,13 @@ function getStylusOptions(loaderContext, loaderOptions) { stylusOptions.filename = loaderContext.resourcePath; + stylusOptions.resolveUrl = + typeof stylusOptions.resolveUrl === 'boolean' && !stylusOptions.resolveUrl + ? false + : typeof stylusOptions.resolveUrl === 'object' + ? stylusOptions.resolveUrl + : {}; + if ( typeof stylusOptions.use !== 'undefined' && stylusOptions.use.length > 0 diff --git a/test/__snapshots__/loader.test.js.snap b/test/__snapshots__/loader.test.js.snap index 4d97e68..fa350cd 100644 --- a/test/__snapshots__/loader.test.js.snap +++ b/test/__snapshots__/loader.test.js.snap @@ -305,7 +305,7 @@ exports[`loader should not be resolved when url begin with "#": css 1`] = ` cursor: pointer; shape-rendering: crispEdges; fill: url(\\"#MyGradient\\"); - background-image: url(\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath d='M256 40c118.621 0 216 96.075 216 216 0 119.291-96.61 216-216 216-119.244 0-216-96.562-216-216 0-119.203 96.602-216 216-216m0-32C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm-11.49 120h22.979c6.823 0 12.274 5.682 11.99 12.5l-7 168c-.268 6.428-5.556 11.5-11.99 11.5h-8.979c-6.433 0-11.722-5.073-11.99-11.5l-7-168c-.283-6.818 5.167-12.5 11.99-12.5zM256 340c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28z'%3E%3C/path%3E%3C/svg%3E\\"); + background-image: url(\\"data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20viewBox=%270%200%20512%20512%27%3E%3Cpath%20d=%27M256%2040c118.621%200%20216%2096.075%20216%20216%200%20119.291-96.61%20216-216%20216-119.244%200-216-96.562-216-216%200-119.203%2096.602-216%20216-216m0-32C119.043%208%208%20119.083%208%20256c0%20136.997%20111.043%20248%20248%20248s248-111.003%20248-248C504%20119.083%20392.957%208%20256%208zm-11.49%20120h22.979c6.823%200%2012.274%205.682%2011.99%2012.5l-7%20168c-.268%206.428-5.556%2011.5-11.99%2011.5h-8.979c-6.433%200-11.722-5.073-11.99-11.5l-7-168c-.283-6.818%205.167-12.5%2011.99-12.5zM256%20340c-15.464%200-28%2012.536-28%2028s12.536%2028%2028%2028%2028-12.536%2028-28-12.536-28-28-28z%27%3E%3C/path%3E%3C/svg%3E\\"); } " `; @@ -718,8 +718,8 @@ th { } @font-face { font-family: \\"Glyphicons Halflings\\"; - src: url(\\"../fonts/glyphicons-halflings-regular.eot\\"); - src: url(\\"../fonts/glyphicons-halflings-regular.eot?#iefix\\") format(\\"embedded-opentype\\"), url(\\"../fonts/glyphicons-halflings-regular.woff2\\") format(\\"woff2\\"), url(\\"../fonts/glyphicons-halflings-regular.woff\\") format(\\"woff\\"), url(\\"../fonts/glyphicons-halflings-regular.ttf\\") format(\\"truetype\\"), url(\\"../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular\\") format(\\"svg\\"); + src: url(\\"../../node_modules/bootstrap-styl/fonts/glyphicons-halflings-regular.eot\\"); + src: url(\\"../../node_modules/bootstrap-styl/fonts/glyphicons-halflings-regular.eot?#iefix\\") format(\\"embedded-opentype\\"), url(\\"../../node_modules/bootstrap-styl/fonts/glyphicons-halflings-regular.woff2\\") format(\\"woff2\\"), url(\\"../../node_modules/bootstrap-styl/fonts/glyphicons-halflings-regular.woff\\") format(\\"woff\\"), url(\\"../../node_modules/bootstrap-styl/fonts/glyphicons-halflings-regular.ttf\\") format(\\"truetype\\"), url(\\"../../node_modules/bootstrap-styl/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular\\") format(\\"svg\\"); } .glyphicon { position: relative; @@ -7251,9 +7251,42 @@ exports[`loader stylus can find modules in node_modules: errors 1`] = `Array []` exports[`loader stylus can find modules in node_modules: warnings 1`] = `Array []`; +exports[`loader with option resolveUrl noCheck, should resolve missing urls relatively: css 1`] = ` +".img { + content: url(\\"img.png\\"); + content: url(\\"not-exist.png\\"); + background: url(\\"deep/deep-img.png\\"); + background: url(\\"!!deep/deep-img.png\\"); + background: url(\\"file!deep/deep-img.png\\"); + background: url(\\"file?foo!deep/deep-img.png\\"); +} +" +`; + +exports[`loader with option resolveUrl noCheck, should resolve missing urls relatively: errors 1`] = `Array []`; + +exports[`loader with option resolveUrl noCheck, should resolve missing urls relatively: warnings 1`] = `Array []`; + +exports[`loader with option, should not resolve urls relatively: css 1`] = ` +".img { + content: url(\\"../img.png\\"); + content: url(\\"../not-exist.png\\"); + background: url(\\"deep-img.png\\"); + background: url(\\"!!deep-img.png\\"); + background: url(\\"file!deep-img.png\\"); + background: url(\\"file?foo!deep-img.png\\"); +} +" +`; + +exports[`loader with option, should not resolve urls relatively: errors 1`] = `Array []`; + +exports[`loader with option, should not resolve urls relatively: warnings 1`] = `Array []`; + exports[`loader with option, should resolve urls relatively: css 1`] = ` ".img { content: url(\\"img.png\\"); + content: url(\\"../not-exist.png\\"); background: url(\\"deep/deep-img.png\\"); background: url(\\"!!deep/deep-img.png\\"); background: url(\\"file!deep/deep-img.png\\"); diff --git a/test/fixtures/deep/deep-urls.styl b/test/fixtures/deep/deep-urls.styl index d53204c..514e6b6 100644 --- a/test/fixtures/deep/deep-urls.styl +++ b/test/fixtures/deep/deep-urls.styl @@ -1,5 +1,6 @@ .img content url(../img.png) + content url(../not-exist.png) background url(deep-img.png) background url(!!deep-img.png) background url(file!deep-img.png) diff --git a/test/loader.test.js b/test/loader.test.js index 8129443..a1c2ac1 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -113,6 +113,38 @@ describe('loader', () => { expect(getErrors(stats)).toMatchSnapshot('errors'); }); + it('with option, should not resolve urls relatively', async () => { + const testId = './shallow.styl'; + const compiler = getCompiler(testId, { + stylusOptions: { + resolveUrl: false, + }, + }); + 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('with option resolveUrl noCheck, should resolve missing urls relatively', async () => { + const testId = './shallow.styl'; + const compiler = getCompiler(testId, { + stylusOptions: { + resolveUrl: { + noCheck: true, + }, + }, + }); + 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('with paths, find deps and load like normal stylus', async () => { const testId = './import-paths.styl'; const compiler = getCompiler(testId, {