Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added option to fix relative urls #186

Merged
merged 3 commits into from
Mar 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/node_modules
/node_modules
.idea/
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ By default, the style-loader appends `<style>` elements to the end of the `<head

If defined, the style-loader will re-use a single `<style>` element, instead of adding/removing individual elements for each required module. **Note:** this option is on by default in IE9, which has strict limitations on the number of style tags allowed on a page. You can enable or disable it with the singleton query parameter (`?singleton` or `?-singleton`).

#### `convertToAbsoluteUrls`

If convertToAbsoluteUrls and sourceMaps are both enabled, relative urls will be converted to absolute urls right before the css is injected into the page. This resolves [an issue](https://github.com/webpack/style-loader/pull/96) where relative resources fail to load when source maps are enabled. You can enable it with the convertToAbsoluteUrls query parameter (`?convertToAbsoluteUrls`).

#### `attrs`

If defined, style-loader will attach given attributes with their values on `<style>` / `<link>` element.
Expand Down Expand Up @@ -113,7 +117,7 @@ So the recommended configuration for webpack is:
}
```

**Note** about source maps support and assets referenced with `url`: when style loader is used with ?sourceMap option, the CSS modules will be generated as `Blob`s, so relative paths don't work (they would be relative to `chrome:blob` or `chrome:devtools`). In order for assets to maintain correct paths setting `output.publicPath` property of webpack configuration must be set, so that absolute paths are generated.
**Note** about source maps support and assets referenced with `url`: when style loader is used with ?sourceMap option, the CSS modules will be generated as `Blob`s, so relative paths don't work (they would be relative to `chrome:blob` or `chrome:devtools`). In order for assets to maintain correct paths setting `output.publicPath` property of webpack configuration must be set, so that absolute paths are generated. Alternatively you can enable the `convertToAbsoluteUrls` option mentioned above.

<h2 align="center">Contributing</h2>

Expand Down
20 changes: 16 additions & 4 deletions addStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ var stylesInDom = {},
}),
singletonElement = null,
singletonCounter = 0,
styleElementsInsertedAtTop = [];
styleElementsInsertedAtTop = [],
fixUrls = require("./fixUrls");

module.exports = function(list, options) {
if(typeof DEBUG !== "undefined" && DEBUG) {
Expand Down Expand Up @@ -59,7 +60,7 @@ module.exports = function(list, options) {
}
}
};
}
};

function addStylesToDom(styles, options) {
for(var i = 0; i < styles.length; i++) {
Expand Down Expand Up @@ -168,7 +169,7 @@ function addStyle(obj, options) {
typeof Blob === "function" &&
typeof btoa === "function") {
styleElement = createLinkElement(options);
update = updateLink.bind(null, styleElement);
update = updateLink.bind(null, styleElement, options);
remove = function() {
removeStyleElement(styleElement);
if(styleElement.href)
Expand Down Expand Up @@ -239,10 +240,21 @@ function applyToTag(styleElement, obj) {
}
}

function updateLink(linkElement, obj) {
function updateLink(linkElement, options, obj) {
var css = obj.css;
var sourceMap = obj.sourceMap;

/* If convertToAbsoluteUrls isn't defined, but sourcemaps are enabled
and there is no publicPath defined then lets turn convertToAbsoluteUrls
on by default. Otherwise default to the convertToAbsoluteUrls option
directly
*/
const autoFixUrls = options.convertToAbsoluteUrls === undefined && sourceMap;

if (options.convertToAbsoluteUrls || autoFixUrls){
css = fixUrls(css);
}

if(sourceMap) {
// http://stackoverflow.com/a/26603875
css += "\n/*# sourceMappingURL=data:application/json;base64," + btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))) + " */";
Expand Down
63 changes: 63 additions & 0 deletions fixUrls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@

/**
* When source maps are enabled, `style-loader` uses a link element with a data-uri to
* embed the css on the page. This breaks all relative urls because now they are relative to a
* bundle instead of the current page.
*
* One solution is to only use full urls, but that may be impossible.
*
* Instead, this function "fixes" the relative urls to be absolute according to the current page location.
*
* A rudimentary test suite is located at `test/fixUrls.js` and can be run via the `npm test` command.
*
*/

module.exports = function (css) {
// get current location
var location = typeof window !== "undefined" && window.location;

if (!location) {
throw new Error("fixUrls requires window.location");
}

// blank or null?
if (!css || typeof css !== "string") {
return css;
}

var baseUrl = location.protocol + "//" + location.host;
var currentDir = baseUrl + location.pathname.replace(/\/[^\/]*$/, "/");

// convert each url(...)
var fixedCss = css.replace(/url *\( *(.+?) *\)/g, function(fullMatch, origUrl) {
// strip quotes (if they exist)
var unquotedOrigUrl = origUrl
.replace(/^"(.*)"$/, function(o, $1){ return $1; })
.replace(/^'(.*)'$/, function(o, $1){ return $1; });

// already a full url? no change
if (/^(#|data:|http:\/\/|https:\/\/|file:\/\/\/)/i.test(unquotedOrigUrl)) {
return fullMatch;
}

// convert the url to a full url
var newUrl;

if (unquotedOrigUrl.indexOf("//") === 0) {
//TODO: should we add protocol?
newUrl = unquotedOrigUrl;
} else if (unquotedOrigUrl.indexOf("/") === 0) {
// path should be relative to the base url
newUrl = baseUrl + unquotedOrigUrl; // already starts with '/'
} else {
// path should be relative to current directory
newUrl = currentDir + unquotedOrigUrl.replace(/^\.\//, ""); // Strip leading './'
}

// send back the fixed url(...)
return "url(" + JSON.stringify(newUrl) + ")";
});

// send back the fixed css
return fixedCss;
};
183 changes: 183 additions & 0 deletions test/fixUrlsTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Node v4 requires "use strict" to allow block scoped let & const
"use strict";
var assert = require("assert");
var url = require('url');

describe("fix urls tests", function() {
var fixUrls = require("../fixUrls");
var defaultUrl = "https://x.y.z/a/b.html";

beforeEach(function() {
global.window = {
location: url.parse(defaultUrl)
};
});

var assertUrl = function (origCss, expectedCss, specialUrl) {
if (specialUrl) {
global.window = {
location: url.parse(specialUrl)
};
}
var resultCss = fixUrls(origCss, specialUrl || defaultUrl);
expectedCss = expectedCss || origCss;

assert.equal(resultCss, expectedCss);
};

// no change
it("Null css is not modified", function() {
assertUrl(null)
});

it("Blank css is not modified", function() { assertUrl("") });

it("No url is not modified", function () { assertUrl("body { }") });

it("Full url isn't changed (no quotes)", function() {
assertUrl("body { background-image:url(http://example.com/bg.jpg); }")
});

it("Full url isn't changed (no quotes, spaces)", function() {
assertUrl("body { background-image:url ( http://example.com/bg.jpg ); }");
});

it("Full url isn't changed (double quotes)", function() {
assertUrl("body { background-image:url(\"http://example.com/bg.jpg\"); }")
});

it("Full url isn't changed (double quotes, spaces)", function() {
assertUrl("body { background-image:url ( \"http://example.com/bg.jpg\" ); }")
});

it("Full url isn't changed (single quotes)", function() {
assertUrl("body { background-image:url('http://example.com/bg.jpg'); }")
});

it("Full url isn't changed (single quotes, spaces)", function() {
assertUrl("body { background-image:url ( 'http://example.com/bg.jpg' ); }")
});

it("Multiple full urls are not changed", function() {
assertUrl(
"body { background-image:url(http://example.com/bg.jpg); }\ndiv.main { background-image:url ( 'https://www.anothersite.com/another.png' ); }"
);
});

it("Http url isn't changed", function() {
assertUrl("body { background-image:url(http://example.com/bg.jpg); }");
});

it("Https url isn't changed", function() {
assertUrl("body { background-image:url(https://example.com/bg.jpg); }");
});

it("HTTPS url isn't changed", function() {
assertUrl("body { background-image:url(HTTPS://example.com/bg.jpg); }")
});

it("File url isn't changed", function() {
assertUrl("body { background-image:url(file:///example.com/bg.jpg); }")
});

it("Double slash url isn't changed", function() {
assertUrl(
"body { background-image:url(//example.com/bg.jpg); }",
"body { background-image:url(\"//example.com/bg.jpg\"); }"
)
});

it("Image data uri url isn't changed", function() {
assertUrl("body { background-image:url(data:image/png;base64,qsrwABYuwNkimqm3gAAAABJRU5ErkJggg==); }")
});

it("Font data uri url isn't changed", function() {
assertUrl(
"body { background-image:url(data:application/x-font-woff;charset=utf-8;base64,qsrwABYuwNkimqm3gAAAABJRU5ErkJggg); }"
);
});

// relative urls
it("Relative url", function() {
assertUrl(
"body { background-image:url(bg.jpg); }",
"body { background-image:url(\"https://x.y.z/a/bg.jpg\"); }"
);
});

it("Relative url with path", function() {
assertUrl(
"body { background-image:url(c/d/bg.jpg); }",
"body { background-image:url(\"https://x.y.z/a/c/d/bg.jpg\"); }"
);
});
it("Relative url with dot slash", function() {
assertUrl(
"body { background-image:url(./c/d/bg.jpg); }",
"body { background-image:url(\"https://x.y.z/a/c/d/bg.jpg\"); }"
);
});

it("Multiple relative urls", function() {
assertUrl(
"body { background-image:url(bg.jpg); }\ndiv.main { background-image:url(./c/d/bg.jpg); }",
"body { background-image:url(\"https://x.y.z/a/bg.jpg\"); }\ndiv.main { background-image:url(\"https://x.y.z/a/c/d/bg.jpg\"); }"
);
});
it("Relative url that looks like data-uri", function() {
assertUrl(
"body { background-image:url(data/image/png.base64); }",
"body { background-image:url(\"https://x.y.z/a/data/image/png.base64\"); }"
);
});

// urls with hashes
it("Relative url with hash are not changed", function() {
assertUrl("body { background-image:url(#bg.jpg); }");
});

// rooted urls
it("Rooted url", function() {
assertUrl(
"body { background-image:url(/bg.jpg); }",
"body { background-image:url(\"https://x.y.z/bg.jpg\"); }"
);
});
it("Rooted url with path", function() {
assertUrl(
"body { background-image:url(/a/b/bg.jpg); }",
"body { background-image:url(\"https://x.y.z/a/b/bg.jpg\"); }"
);
});

//special locations
it("Location with no path, filename only", function() {
assertUrl(
"body { background-image:url(bg.jpg); }",
"body { background-image:url(\"http://x.y.z/bg.jpg\"); }",
"http://x.y.z"
);
});

it("Location with no path, path with filename", function() {
assertUrl(
"body { background-image:url(a/bg.jpg); }",
"body { background-image:url(\"http://x.y.z/a/bg.jpg\"); }",
"http://x.y.z"
);
});
it("Location with no path, rel path with filename", function() {
assertUrl(
"body { background-image:url(./a/bg.jpg); }",
"body { background-image:url(\"http://x.y.z/a/bg.jpg\"); }",
"http://x.y.z"
);
});
it("Location with no path, root filename", function() {
assertUrl(
"body { background-image:url(/a/bg.jpg); }",
"body { background-image:url(\"http://x.y.z/a/bg.jpg\"); }",
"http://x.y.z"
);
});
});