From 420bd9e3aaae9a2deddc09fffbea0d4381f53dd4 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 18 Jun 2015 15:55:29 +0200 Subject: [PATCH 1/6] Use PostCSS Use new CSS Modules postcss stuff fixes #54 fixes #60 --- README.md | 43 +- lib/ReplaceMany.js | 29 -- lib/generateLocals.js | 38 -- lib/loader.js | 185 ++++---- lib/localsLoader.js | 43 +- lib/parseSource.js | 419 ------------------- lib/processCss.js | 188 +++++++++ lib/processLocals.js | 39 -- package.json | 10 +- test/helpers.js | 31 +- test/importTest.js | 8 +- test/localTest.js | 20 +- test/localsTest.js | 36 ++ test/moduleTest.js | 2 +- test/moduleTestCases/leak-scope/expected.css | 6 +- test/moduleTestCases/leak-scope/source.css | 10 +- 16 files changed, 408 insertions(+), 699 deletions(-) delete mode 100644 lib/ReplaceMany.js delete mode 100644 lib/generateLocals.js delete mode 100644 lib/parseSource.js create mode 100644 lib/processCss.js delete mode 100644 lib/processLocals.js create mode 100644 test/localsTest.js diff --git a/README.md b/README.md index 03cf41bd..8a3b7266 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ var css = require("css!./file.css"); Good loaders for requiring your assets are the [file-loader](https://github.com/webpack/file-loader) and the [url-loader](https://github.com/webpack/url-loader) which you should specify in your config (see below). -To be compatible with existing css files: +To be compatible with existing css files (if not in CSS Module mode): * `url(image.png)` => `require("./image.png")` * `url(~module/image.png)` => `require("module/image.png")` @@ -58,6 +58,8 @@ The result is: * `url(/image.png)` => `require("./image.png")` +Using 'Root-relative' urls is not recommended. You should only use it for legacy CSS files. + ### Local scope By default CSS exports all class names into a global selector scope. This is a feature which offer a local selector scope. @@ -97,28 +99,30 @@ exports.locals = { Camelcasing is recommended for local selectors. They are easier to use in the importing javascript module. +`url(...)` URLs in block scoped (`:local .abc`) rules behave like requests in modules: + * `./file.png` instead of `file.png` + * `module/file.png` instead of `~module/file.png` + + You can use `:local(#someId)`, but this is not recommended. Use classes instead of ids. You can configure the generated ident with the `localIdentName` query parameter (default `[hash:base64]`). Example: `css-loader?localIdentName=[path][name]---[local]---[hash:base64:5]` for easier debugging. -Note: For prerendering with extract-text-webpack-plugin you should use `css-loader/locals` instead of `style-loader!css-loader` in the prerendering bundle. It doesn't embed CSS but only exports the identifier mappings. +Note: For prerendering with extract-text-webpack-plugin you should use `css-loader/locals` instead of `style-loader!css-loader` **in the prerendering bundle**. It doesn't embed CSS but only exports the identifier mappings. ### Module mode (experimental) -The query parameter `module` enables **CSS Module** mode. (`css-loader?module`) +See [CSS Modules](https://github.com/css-modules/css-modules). -* Local scoped by default. -* `url(...)` URLs behave like requests in modules: - * `./file.png` instead of `file.png` - * `module/file.png` instead of `~module/file.png` +The query parameter `module` enables **CSS Module** mode. (`css-loader?module`) -Thanks to [@markdalgleish](https://github.com/markdalgleish) for prior work on this topic. +This enables Local scoped CSS by default. (You can leave it with `:global(...)` or `:global` for selectors and/or rules.) -### Inheriting +### Composing CSS classes -When declaring a local class name you can inherit from another local class name. +When declaring a local class name you can compose a local class from another local class name. ``` css :local(.className) { @@ -127,7 +131,7 @@ When declaring a local class name you can inherit from another local class name. } :local(.subClass) { - extends: className; + composes: className; background: blue; } ``` @@ -160,26 +164,25 @@ To import a local class name from another module: ``` css :local(.continueButton) { - extends: button from "library/button.css"; + composes: button from "library/button.css"; background: red; } ``` ``` css :local(.nameEdit) { - extends: edit highlight from "./edit.css"; + composes: edit highlight from "./edit.css"; background: red; } ``` -To import from multiple modules use multiple `extends:` rules. You can also use `url(...)` to specify the module (it behave a bit different). +To import from multiple modules use multiple `composes:` rules. ``` css :local(.className) { - extends: edit hightlight from "./edit.css"; - extends: button from url("button.css"); - /* equal to 'extends: button from "./button.css";' */ - extends: classFromThisModule; + composes: edit hightlight from "./edit.css"; + composes: button from "module/button.css"; + composes: classFromThisModule; background: red; } ``` @@ -192,6 +195,8 @@ To include SourceMaps set the `sourceMap` query param. I. e. the extract-text-webpack-plugin can handle them. +They are not enabled by default because they expose a runtime overhead and increase in bundle size (JS SourceMap do not). In addition to that relative paths are buggy and you need to use an absolute public path which include the server url. + ### importing and chained loaders The query parameter `importLoaders` allow to configure which loaders should be applied to `@import`ed resources. @@ -210,6 +215,8 @@ require("style-loader!css-loader!stylus-loader!...") require("css-loader!...") ``` +This may change in the future, when the module system (i. e. webpack) supports loader matching by origin. + ### Minification By default the css-loader minimizes the css if specified by the module system. diff --git a/lib/ReplaceMany.js b/lib/ReplaceMany.js deleted file mode 100644 index 5e98257c..00000000 --- a/lib/ReplaceMany.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ -function ReplaceMany() { - this.replacements = []; -} - -module.exports = ReplaceMany; - -ReplaceMany.prototype.replace = function(start, length, newString) { - this.replacements.push([start, start + length, newString]); - return this; -}; - -ReplaceMany.prototype.run = function(string) { - this.replacements.sort(function(a, b) { - return b[0] - a[0]; - }); - var result = [string]; - this.replacements.forEach(function(repl) { - var str = result.pop(); - var done = str.substr(repl[1]); - var leftover = str.substr(0, repl[0]); - result.push(done, repl[2], leftover); - }); - result.reverse(); - return result.join(""); -}; diff --git a/lib/generateLocals.js b/lib/generateLocals.js deleted file mode 100644 index 6b2b5e93..00000000 --- a/lib/generateLocals.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ -var loaderUtils = require("loader-utils"); -module.exports = function(locals, localExtends, importedUrls, importUrlPrefix, result, importAccess) { - var localKeys = Object.keys(locals); - if(localKeys.length > 0) { - var localLines = localKeys.map(function(key, idx) { - var line = " " + JSON.stringify(key) + ": "; - function addExtend(extend) { - if(extend.from) { - var importUrl = importUrlPrefix + - (extend.fromType === "url" ? loaderUtils.urlToRequest(extend.from) : extend.from); - if(importedUrls && result && importedUrls.indexOf(importUrl) < 0) { - result.push("exports.i(require(" + loaderUtils.stringifyRequest(this, importUrl) + "), \"\");"); - importedUrls.push(importUrl); - } - line += " + \" \" + require(" + loaderUtils.stringifyRequest(this, importUrl) + ")" + importAccess + "[" + JSON.stringify(extend.name) + "]"; - } else if(locals[extend.name]) { - line += " + \" \" + " + JSON.stringify(locals[extend.name]); - if(localExtends[extend.name]) { - localExtends[extend.name].forEach(addExtend, this); - } - } else if(this.emitError) { - this.emitError("Cannot extend from unknown class '" + extend.name + "'"); - } - } - line += JSON.stringify(locals[key]); - if(localExtends[key]) { - localExtends[key].forEach(addExtend, this); - } - if(idx !== localKeys.length - 1) line += ","; - return line; - }, this); - return "{\n" + localLines.join("\n") + "\n}"; - } -}; diff --git a/lib/loader.js b/lib/loader.js index 6f1fcc5f..0e61d179 100644 --- a/lib/loader.js +++ b/lib/loader.js @@ -3,26 +3,31 @@ Author Tobias Koppers @sokra */ var path = require("path"); -var parseSource = require("./parseSource"); -var ReplaceMany = require("./ReplaceMany"); var loaderUtils = require("loader-utils"); -var SourceListMap = require("source-list-map").SourceListMap; -var CleanCSS = require("clean-css"); +var processCss = require("./processCss"); + module.exports = function(content, map) { if(this.cacheable) this.cacheable(); var query = loaderUtils.parseQuery(this.query); var root = query.root; - var forceMinimize = query.minimize; var importLoaders = parseInt(query.importLoaders, 10) || 0; - var minimize = typeof forceMinimize !== "undefined" ? !!forceMinimize : (this && this.minimize); var moduleMode = query.module; if(typeof map !== "string") { map = JSON.stringify(map); } - var result = []; + var result = processCss(content, map, { + mode: moduleMode ? "local" : "global", + from: loaderUtils.getRemainingRequest(this), + to: loaderUtils.getCurrentRequest(this), + query: query, + minimize: this.minimize, + loaderContext: this + }); + + var cssAsString = JSON.stringify(result.source); // for importing CSS var loadersRequest = this.loaders.slice( @@ -31,132 +36,88 @@ module.exports = function(content, map) { ).map(function(x) { return x.request; }).join("!"); var importUrlPrefix = "-!" + loadersRequest + "!"; - var stuff = parseSource(content); - - var replacer = new ReplaceMany(); - - // store already imported files - var importedUrls = []; - - // add @imports to result - stuff.imports.forEach(function(imp) { - replacer.replace(imp.start, imp.length, ""); + var alreadyImported = {}; + var importJs = result.importItems.filter(function(imp) { + if(!imp.mediaQuery) { + if(alreadyImported[imp.url]) + return false; + alreadyImported[imp.url] = true; + } + return true; + }).map(function(imp) { if(!loaderUtils.isUrlRequest(imp.url, root)) { - result.push("exports.push([module.id, " + + return "exports.push([module.id, " + JSON.stringify("@import url(" + imp.url + ");") + ", " + - JSON.stringify(imp.mediaQuery) + "]);"); + JSON.stringify(imp.mediaQuery) + "]);"; } else { - var importUrl = importUrlPrefix + - (moduleMode ? imp.url : loaderUtils.urlToRequest(imp.url)); - result.push("exports.i(require(" + loaderUtils.stringifyRequest(this, importUrl) + "), " + JSON.stringify(imp.mediaQuery) + ");"); - if(!imp.mediaQuery) - importedUrls.push(importUrl); + var importUrl = importUrlPrefix + imp.url; + return "exports.i(require(" + loaderUtils.stringifyRequest(this, importUrl) + "), " + JSON.stringify(imp.mediaQuery) + ");"; } - }, this); - - // replace url(...) - if(query.url !== false) { - stuff.urls.forEach(function(url, idx) { - replacer.replace(url.start, url.length, "__CSSLOADERURL_" + idx + "__"); - }); - } - - // replace :local() - var locals = {}; - var localExtends = {}; - require("./processLocals").call(this, stuff.selectors, query, replacer, locals, localExtends); - - // remove stuff - stuff.remove.forEach(function(rem) { - replacer.replace(rem.start, rem.length, ""); - }); - - // pass errors from parser - if(this.emitError) { - stuff.errors.forEach(function(err) { - this.emitError(err); - }, this); + }).join("\n"); + + function importItemMatcher(item) { + var match = result.importItemRegExp.exec(item); + var idx = +match[1]; + var importItem = result.importItems[idx]; + var importUrl = importUrlPrefix + importItem.url; + return "\" + require(" + loaderUtils.stringifyRequest(this, importUrl) + ").locals" + + "[" + JSON.stringify(importItem.export) + "] + \""; } - // generate the locals - var localsData = require("./generateLocals").call(this, locals, localExtends, importedUrls, importUrlPrefix, result, ".locals"); - if(localsData) { - result.push("exports.locals = " + localsData + ";"); - } - - // transform the CSS - var cssContent = replacer.run(content); - - // minimize CSS - if(minimize) { - var options = Object.create(query); - if(query.sourceMap && map) { - options.sourceMap = map; - } - var minimizeResult = new CleanCSS(options).minify(cssContent); - map = minimizeResult.sourceMap; - cssContent = minimizeResult.styles; - if(typeof map !== "string") - map = JSON.stringify(map); - } - - function toEmbStr(str) { - return JSON.stringify(str).replace(/^"|"$/g, ""); - } - - // replace url(...) in the generated code - var css = JSON.stringify(cssContent); - var urlRegExp = /__CSSLOADERURL_[0-9]+__/g; - css = css.replace(urlRegExp, function(str) { - var match = /^__CSSLOADERURL_([0-9]+)__$/.exec(str); - if(!match) return str; - var idx = parseInt(match[1], 10); - if(!stuff.urls[idx]) return str; - var urlItem = stuff.urls[idx]; + cssAsString = cssAsString.replace(result.importItemRegExpG, importItemMatcher.bind(this)).replace(result.urlItemRegExpG, function(item) { + var match = result.urlItemRegExp.exec(item); + var idx = +match[1]; + var urlItem = result.urlItems[idx]; var url = urlItem.url; - if(!loaderUtils.isUrlRequest(url, root)) - return toEmbStr(urlItem.raw); idx = url.indexOf("?#"); if(idx < 0) idx = url.indexOf("#"); var urlRequest; if(idx > 0) { // idx === 0 is catched by isUrlRequest // in cases like url('webfont.eot?#iefix') urlRequest = url.substr(0, idx); - if(!moduleMode) urlRequest = loaderUtils.urlToRequest(urlRequest, root); - return "\"+require(" + loaderUtils.stringifyRequest(this, urlRequest) + ")+\"" + url.substr(idx); + return "\" + require(" + loaderUtils.stringifyRequest(this, urlRequest) + ") + \"" + + url.substr(idx); } urlRequest = url; - if(!moduleMode) urlRequest = loaderUtils.urlToRequest(url, root); - return "\"+require(" + loaderUtils.stringifyRequest(this, urlRequest) + ")+\""; + return "\" + require(" + loaderUtils.stringifyRequest(this, urlRequest) + ") + \""; }.bind(this)); - // add a SourceMap - if(query.sourceMap && !minimize) { - var cssRequest = loaderUtils.getRemainingRequest(this); - var request = loaderUtils.getCurrentRequest(this); - if(!map) { - var sourceMap = new SourceListMap(); - sourceMap.add(content, cssRequest, content); - map = sourceMap.toStringWithSourceMap({ - file: request - }).map; - if(map.sources) { - map.sources = map.sources.map(function(source) { - var p = path.relative(query.context || this.options.context, source).replace(/\\/g, "/"); - if(p.indexOf("../") !== 0) - p = "./" + p; - return "/" + p; - }, this); - map.sourceRoot = "webpack://"; - } - map = JSON.stringify(map); + var exportJs = ""; + if(Object.keys(result.exports).length > 0) { + exportJs = Object.keys(result.exports).map(function(key) { + var valueAsString = JSON.stringify(result.exports[key]); + valueAsString = valueAsString.replace(result.importItemRegExpG, importItemMatcher.bind(this)); + return "\t" + JSON.stringify(key) + ": " + valueAsString; + }.bind(this)).join(",\n"); + exportJs = "exports.locals = {\n" + exportJs + "\n};"; + } + + + var moduleJs; + if(query.sourceMap && result.map) { + // add a SourceMap + map = result.map; + if(map.sources) { + map.sources = map.sources.map(function(source) { + var p = path.relative(query.context || this.options.context, source).replace(/\\/g, "/"); + if(p.indexOf("../") !== 0) + p = "./" + p; + return "/" + p; + }, this); + map.sourceRoot = "webpack://"; } - result.push("exports.push([module.id, " + css + ", \"\", " + map + "]);"); + map = JSON.stringify(map); + moduleJs = "exports.push([module.id, " + cssAsString + ", \"\", " + map + "]);"; } else { - result.push("exports.push([module.id, " + css + ", \"\"]);"); + moduleJs = "exports.push([module.id, " + cssAsString + ", \"\"]);"; } // embed runtime return "exports = module.exports = require(" + loaderUtils.stringifyRequest(this, require.resolve("./css-base.js")) + ")();\n" + - result.join("\n"); + "// imports\n" + + importJs + "\n\n" + + "// module\n" + + moduleJs + "\n\n" + + "// exports\n" + + exportJs; }; diff --git a/lib/localsLoader.js b/lib/localsLoader.js index 8bc067f4..ece70f65 100644 --- a/lib/localsLoader.js +++ b/lib/localsLoader.js @@ -3,12 +3,21 @@ Author Tobias Koppers @sokra */ var loaderUtils = require("loader-utils"); -var parseSource = require("./parseSource"); +var processCss = require("./processCss"); + module.exports = function(content) { if(this.cacheable) this.cacheable(); var query = loaderUtils.parseQuery(this.query); var importLoaders = parseInt(query.importLoaders, 10) || 0; + var moduleMode = query.module; + + var result = processCss(content, null, { + mode: moduleMode ? "local" : "global", + query: query, + minimize: this.minimize, + loaderContext: this + }); // for importing CSS var loadersRequest = this.loaders.slice( @@ -17,16 +26,24 @@ module.exports = function(content) { ).map(function(x) { return x.request; }).join("!"); var importUrlPrefix = "-!" + loadersRequest + "!"; - var stuff = parseSource(content); - - var locals = {}; - var localExtends = {}; - require("./processLocals").call(this, stuff.selectors, query, null, locals, localExtends); - - - // generate the locals - var localsData = require("./generateLocals").call(this, locals, localExtends, null, importUrlPrefix, null, ""); - - - return "module.exports = " + localsData + ";"; + function importItemMatcher(item) { + var match = result.importItemRegExp.exec(item); + var idx = +match[1]; + var importItem = result.importItems[idx]; + var importUrl = importUrlPrefix + importItem.url; + return "\" + require(" + loaderUtils.stringifyRequest(this, importUrl) + ")" + + "[" + JSON.stringify(importItem.export) + "] + \""; + } + + var exportJs = ""; + if(Object.keys(result.exports).length > 0) { + exportJs = Object.keys(result.exports).map(function(key) { + var valueAsString = JSON.stringify(result.exports[key]); + valueAsString = valueAsString.replace(result.importItemRegExpG, importItemMatcher.bind(this)); + return "\t" + JSON.stringify(key) + ": " + valueAsString; + }.bind(this)).join(",\n"); + exportJs = "module.exports = {\n" + exportJs + "\n};"; + } + + return exportJs; }; diff --git a/lib/parseSource.js b/lib/parseSource.js deleted file mode 100644 index 178012a6..00000000 --- a/lib/parseSource.js +++ /dev/null @@ -1,419 +0,0 @@ -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ -var loaderUtils = require("loader-utils"); -var Parser = require("fastparse"); - -function errorMatch(message, newParserMode) { - return function(match) { - var index = arguments[arguments.length - 1]; - var nextLine = this.source.indexOf("\n", index); - var splittedSource = this.source.substr(0, index).split("\n"); - var line = splittedSource.length; - var lineBeforeError = splittedSource.pop(); - var lineAfterError = this.source.substr(index, nextLine); - this.errors.push("Unexpected '" + match + "' in line " + (line + 1) + ", " + message + "\n" + - lineBeforeError + lineAfterError + "\n" + lineBeforeError.replace(/[^\\s]/g, " ") + "^"); - return newParserMode; - }; -} - -function urlMatch(match, textBeforeUrl, replacedText, url, index) { - this.urls.push({ - url: url, - raw: replacedText, - start: index + textBeforeUrl.length, - length: replacedText.length - }); -} - -function importMatch(match, url, mediaQuery, index) { - this.imports.push({ - url: url, - mediaQuery: mediaQuery, - start: index, - length: match.length - }); -} - -function rulesStartMatch() { - this.blockMode = this.mode; - return "firstRule"; -} - -function rulesEndMatch() { - this.mode = null; - this.activeSelectors = []; - return "source"; -} - -function nextSelectorMatch() { - this.mode = null; -} - -function enableLocal(match, whitespace, index) { - this.remove.push({ - start: index + whitespace.length, - length: match.length - whitespace.length - }); - this.mode = "local"; -} - -function enableGlobal(match, whitespace, index) { - this.remove.push({ - start: index + whitespace.length, - length: match.length - whitespace.length - }); - this.mode = "global"; -} - -function localStart(match, whitespace, index) { - this.remove.push({ - start: index + whitespace.length, - length: match.length - whitespace.length - }); - this.bracketStatus = 0; - return "local"; -} - -function globalStart(match, whitespace, index) { - this.remove.push({ - start: index + whitespace.length, - length: match.length - whitespace.length - }); - this.bracketStatus = 0; - return "global"; -} - -function withMode(mode, fn) { - return function() { - var oldMode = this.mode; - this.mode = mode; - var newParserMode = fn.apply(this, arguments); - this.mode = oldMode; - return newParserMode; - }; -} - -function jump(newParserMode, fn) { - return function() { - fn.apply(this, arguments); - return newParserMode; - }; -} - -function innerBracketIn() { - this.bracketStatus++; -} - -function innerBracketOut(match, index) { - if(this.bracketStatus-- === 0) { - this.remove.push({ - start: index, - length: match.length - }); - return "source"; - } -} - -function selectorMatch(match, prefix, name, index) { - var selector = { - name: name, - prefix: prefix, - start: index, - length: match.length, - mode: this.mode - }; - this.selectors.push(selector); - this.activeSelectors.push(selector); -} - -function ruleScopedMatch() { - this.mode = this.blockMode; - return "ruleScoped"; -} - -function extendsStartMatch(match, index) { - this.remove.push({ - start: index, - length: 0 - }); - this.activeExtends = []; - return "extends"; -} - -function extendsEndMatch(match, index) { - var lastRemove = this.remove[this.remove.length - 1]; - lastRemove.length = index + match.length - lastRemove.start; - if(this.activeExtends.length === 0) { - errorMatch("expected class names")(match, index); - return "rule"; - } - return "firstRule"; -} - -function extendsClassNameMatch(match, name, index) { - this.activeSelectors.forEach(function(selector) { - if(!selector.extends) - selector.extends = []; - var extend = { - name: name, - start: index, - length: match.length, - from: null, - fromType: null - }; - selector.extends.push(extend); - this.activeExtends.push(extend); - }, this); -} - -function extendsFromUrlMatch(match, request) { - this.activeExtends.forEach(function(extend) { - extend.from = request; - extend.fromType = "url"; - }); -} - -function extendsFromMatch(match, request) { - this.activeExtends.forEach(function(extend) { - extend.from = loaderUtils.parseString(request); - extend.fromType = "module"; - }); -} - -var parser = new Parser({ - // shared stuff - comments: { - "/\\*[\\s\\S]*?\\*/": true - }, - strings: { - '"([^\\\\"]|\\\\.)*"': true, - "'([^\\\\']|\\\\.)*'": true - }, - urls: { - '(url\\s*\\()(\\s*"([^"]*)"\\s*)\\)': urlMatch, - "(url\\s*\\()(\\s*'([^']*)'\\s*)\\)": urlMatch, - "(url\\s*\\()(\\s*([^)]*)\\s*)\\)": urlMatch - }, - scopedRules: { - "(?:-[a-z]+-)?animation(?:-name)?:": ruleScopedMatch - }, - - // states - source: [ - "comments", - "strings", - "urls", - { - // imports - '@\\s*import\\s+"([^"]*)"\\s*([^;\\n]*);': importMatch, - "@\\s*import\\s+'([^'']*)'\\s*([^;\\n]*);": importMatch, - '@\\s*import\\s+url\\s*\\(\\s*"([^"]*)"\\s*\\)\\s*([^;\\n]*);': importMatch, - "@\\s*import\\s+url\\s*\\(\\s*'([^']*)'\\s*\\)\\s*([^;\\n]*);": importMatch, - "@\\s*import\\s+url\\s*\\(\\s*([^)]*)\\s*\\)\\s*([^;\\n]*);": importMatch, - - // charset - "@charset": true, - - // namespace - "@(?:-[a-z]+-)?namespace": true, - - // atrule - "@(?:-[a-z]+-)?keyframes": "atruleScoped", - "@": "atrule" - }, - { - // local - "(\\s*):local\\(": localStart, - "():local": enableLocal, - "(\\s+):local\\s+": enableLocal, - - // global - "(\\s*):global\\(": globalStart, - "():global": enableGlobal, - "(\\s+):global\\s+": enableGlobal, - - // class - "(\\.)(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)": selectorMatch, - - // id - "(#)(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)": selectorMatch, - - // inside - "\\{": rulesStartMatch, - - ",": nextSelectorMatch - } - ], - atruleScoped: [ - "comments", - "strings", - { - // identifier - ":local\\(\\s*()([A-Za-z_\\-0-9]+)\\s*\\)": withMode("local", selectorMatch), - ":global\\(\\s*()([A-Za-z_\\-0-9]+)\\s*\\)": withMode("global", selectorMatch), - "()([A-Za-z_\\-0-9]+)": selectorMatch, - - // local - "():local": enableLocal, - "(\\s+):local\\s+": enableLocal, - - // global - "():global": enableGlobal, - "(\\s+):global\\s+": enableGlobal, - - // back to normal source - "\\{": "source" - } - ], - atrule: [ - "comments", - "strings", - { - // back to normal source - "\\{": "source" - } - ], - ruleScoped: [ - "comments", - { - // identifier - ":local\\(\\s*()([A-Za-z_\\-0-9]+)\\s*\\)": jump("ruleScopedInactive", withMode("local", selectorMatch)), - ":global\\(\\s*()([A-Za-z_\\-0-9]+)\\s*\\)": jump("ruleScopedInactive", withMode("global", selectorMatch)), - "()([A-Za-z_\\-0-9]+)": jump("ruleScopedInactive", selectorMatch), - - // local - "():local": enableLocal, - "(\\s+):local\\s+": enableLocal, - - // global - "():global": enableGlobal, - "(\\s+):global\\s+": enableGlobal, - - // back to normal rule - ";": "rule", - - // back to normal source - "\\}": rulesEndMatch - } - ], - ruleScopedInactive: [ - "comments", - { - // reactivate - ",": ruleScopedMatch, - - // back to normal rule - ";": "rule", - - // back to normal source - "\\}": rulesEndMatch - } - ], - rule: [ - "comments", - "strings", - "urls", - "scopedRules", - { - // back to normal source - "\\}": rulesEndMatch - } - ], - firstRule: [ - "rule", - { - "extends\\s*:": extendsStartMatch, - - // whitespace - "\\s+": true, - - // url - ".": "rule" - } - ], - extends: [ - "comments", - { - ";\\s*": extendsEndMatch, - - // whitespace - "\\s+": true, - - // from - "from": "extendsFrom", - - // class name - "([A-Za-z_\\-0-9]+)": extendsClassNameMatch, - - ".+[;}]": errorMatch("expected class names or 'from'", "rule"), - ".": errorMatch("expected class names or 'from'", "rule") - } - ], - extendsFrom: [ - "comments", - { - ";\\s*": extendsEndMatch, - - // whitespace - "\\s+": true, - - // module - 'url\\s*\\(\\s*"([^"]*)"\\s*\\)': extendsFromUrlMatch, - "url\\s*\\(\\s*'([^']*)'\\s*\\)": extendsFromUrlMatch, - "url\\s*\\(\\s*([^)]*)\\s*\\)": extendsFromUrlMatch, - '("(?:[^\\\\"]|\\\\.)*")': extendsFromMatch, - "('(?:[^\\\\']|\\\\.)*')": extendsFromMatch, - - ".+[;}]": errorMatch("expected module identifier (a string or 'url(...)'')", "rule"), - ".": errorMatch("expected module identifier (a string or 'url(...)'')", "rule") - } - ], - local: [ - "comments", - "strings", - { - // class - "(\\.)([A-Za-z_\\-0-9]+)": withMode("local", selectorMatch), - - // id - "(#)([A-Za-z_\\-0-9]+)": withMode("local", selectorMatch), - - // brackets - "\\(": innerBracketIn, - "\\)": innerBracketOut - } - ], - global: [ - "comments", - "strings", - { - // class - "(\\.)([A-Za-z_\\-0-9]+)": withMode("global", selectorMatch), - - // id - "(#)([A-Za-z_\\-0-9]+)": withMode("global", selectorMatch), - - // brackets - "\\(": innerBracketIn, - "\\)": innerBracketOut - } - ] -}); - -module.exports = function parseSource(source) { - var context = { - source: source, - imports: [], - urls: [], - selectors: [], - remove: [], - errors: [], - activeSelectors: [], - bracketStatus: 0, - mode: null - }; - return parser.parse("source", source, context); -}; diff --git a/lib/processCss.js b/lib/processCss.js new file mode 100644 index 00000000..4f5e38df --- /dev/null +++ b/lib/processCss.js @@ -0,0 +1,188 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ +var Tokenizer = require("css-selector-tokenizer"); +var postcss = require("postcss"); +var loaderUtils = require("loader-utils"); +var getLocalIdent = require("./getLocalIdent"); + +var localByDefault = require("postcss-modules-local-by-default"); +var extractImports = require("postcss-modules-extract-imports"); +var modulesScope = require("postcss-modules-scope"); +var cssnano = require("cssnano"); + +var parserPlugin = postcss.plugin("css-loader-parser", function(options) { + return function(css) { + var imports = {}; + var exports = {}; + var importItems = []; + var urlItems = []; + + function replaceImportsInString(str) { + var tokens = str.split(/(\w+)/); + tokens = tokens.map(function(token) { + var importIndex = imports["$" + token]; + if(typeof importIndex === "number") { + return "___CSS_LOADER_IMPORT___" + importIndex + "___"; + } + return token; + }); + return tokens.join(""); + } + + css.eachAtRule("import", function(rule) { + var values = Tokenizer.parseValues(rule.params); + var url = values.nodes[0].nodes[0]; + if(url.type === "url") { + url = url.url; + } else if(url.type === "string") { + url = url.value; + } else throw rule.error("Unexpected format" + rule.params); + values.nodes[0].nodes.shift(); + var mediaQuery = Tokenizer.stringifyValues(values); + if(loaderUtils.isUrlRequest(url, options.root) && options.mode === "global") { + url = loaderUtils.urlToRequest(url, options.root); + } + importItems.push({ + url: url, + mediaQuery: mediaQuery + }); + rule.removeSelf(); + }); + + css.eachRule(function(rule) { + if(rule.selector === ":export") { + rule.eachDecl(function(decl) { + exports[decl.prop] = decl.value; + }); + rule.removeSelf(); + } else if(/^:import\(.+\)$/.test(rule.selector)) { + var match = /^:import\((.+)\)$/.exec(rule.selector); + var url = loaderUtils.parseString(match[1]); + rule.eachDecl(function(decl) { + imports["$" + decl.prop] = importItems.length; + importItems.push({ + url: url, + export: decl.value + }); + }); + rule.removeSelf(); + } + }); + + Object.keys(exports).forEach(function(exportName) { + exports[exportName] = replaceImportsInString(exports[exportName]); + }); + + css.eachDecl(function(decl) { + var values = Tokenizer.parseValues(decl.value); + values.nodes.forEach(function(value) { + value.nodes.forEach(function(item) { + switch(item.type) { + case "item": + var importIndex = imports["$" + item.name]; + if(typeof importIndex === "number") { + item.name = "___CSS_LOADER_IMPORT___" + importIndex + "___"; + } + break; + case "url": + if(!/^#/.test(item.url) && loaderUtils.isUrlRequest(item.url, options.root)) { + item.stringType = ""; + delete item.innerSpacingBefore; + delete item.innerSpacingAfter; + var url = item.url; + item.url = "___CSS_LOADER_URL___" + urlItems.length + "___"; + urlItems.push({ + url: url + }); + } + break; + } + }); + }); + decl.value = Tokenizer.stringifyValues(values); + }); + css.eachAtRule(function(atrule) { + if(typeof atrule.params === "string") { + atrule.params = replaceImportsInString(atrule.params); + } + }); + + options.importItems = importItems; + options.urlItems = urlItems; + options.exports = exports; + }; +}); + +module.exports = function processCss(inputSource, inputMap, options) { + + var query = options.query; + var root = query.root; + var localIdentName = query.localIdentName || "[hash:base64]"; + var localIdentRegExp = query.localIdentRegExp; + var forceMinimize = query.minimize; + var minimize = typeof forceMinimize !== "undefined" ? !!forceMinimize : options.minimize; + + + var parserOptions = { + root: root, + mode: options.mode + }; + + var pipeline = postcss([ + localByDefault({ + mode: options.mode, + rewriteUrl: function(global, url) { + if(!loaderUtils.isUrlRequest(url, root)) { + return url; + } + if(global) { + return loaderUtils.urlToRequest(url, root); + } + return url; + } + }), + extractImports({ + createImportedName: function(importName) { + return "___" + importName; + } + }), + modulesScope({ + generateScopedName: function(exportName) { + return getLocalIdent(options.loaderContext, localIdentName, exportName, { + regExp: localIdentRegExp + }); + } + }), + parserPlugin(parserOptions) + ]); + + if(minimize) { + pipeline.use(cssnano()); + } + + var result = pipeline.process(inputSource, { + from: options.from, + to: options.to, + map: { + prev: inputMap, + sourcesContent: true, + inline: false, + annotation: false + } + }); + + + return { + source: result.css, + map: result.map && result.map.toJSON(), + exports: parserOptions.exports, + importItems: parserOptions.importItems, + importItemRegExpG: /___CSS_LOADER_IMPORT___([0-9]+)___/g, + importItemRegExp: /___CSS_LOADER_IMPORT___([0-9]+)___/, + urlItems: parserOptions.urlItems, + urlItemRegExpG: /___CSS_LOADER_URL___([0-9]+)___/g, + urlItemRegExp: /___CSS_LOADER_URL___([0-9]+)___/ + }; +}; diff --git a/lib/processLocals.js b/lib/processLocals.js deleted file mode 100644 index c98fc8c2..00000000 --- a/lib/processLocals.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Tobias Koppers @sokra -*/ -var getLocalIdent = require("./getLocalIdent"); -module.exports = function(parsedLocals, query, replacer, locals, localExtends) { - var localIdentName = query.localIdentName || "[hash:base64]"; - var localIdentRegExp = query.localIdentRegExp; - var moduleMode = query.module; - - parsedLocals.forEach(function(selector) { - if(moduleMode) { - if(selector.mode === "global") - return; - } else { - if(selector.mode !== "local") - return; - } - var ident; - var name = selector.name; - if(!locals[name]) { - ident = getLocalIdent(this, localIdentName, name, { - regExp: localIdentRegExp - }); - locals[name] = ident; - } else { - ident = locals[name]; - } - if(selector.extends) { - selector.extends.forEach(function(extend) { - if(!localExtends[name]) - localExtends[name] = []; - localExtends[name].push(extend); - }); - } - if(replacer) - replacer.replace(selector.start, selector.length, selector.prefix + ident); - }, this); -}; diff --git a/package.json b/package.json index 43e4f1d4..f8a06d85 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,13 @@ "author": "Tobias Koppers @sokra", "description": "css loader module for webpack", "dependencies": { - "clean-css": "^3.1.9", - "fastparse": "^1.1.1", + "css-selector-tokenizer": "^0.5.1", + "cssnano": "^1.4.2", "loader-utils": "~0.2.2", + "postcss": "^4.1.11", + "postcss-modules-extract-imports": "0.0.5", + "postcss-modules-local-by-default": "0.0.10", + "postcss-modules-scope": "0.0.7", "source-list-map": "^0.1.4" }, "devDependencies": { @@ -14,7 +18,7 @@ "coveralls": "^2.11.2", "istanbul": "^0.3.13", "mocha": "^2.2.4", - "should": "^5.2.0" + "should": "^7.0.1" }, "scripts": { "test": "mocha", diff --git a/test/helpers.js b/test/helpers.js index 59c065c1..1dd7bfee 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -2,14 +2,15 @@ require("should"); var cssLoader = require("../index.js"); +var cssLoaderLocals = require("../locals.js"); var vm = require("vm"); -function getEvaluated(output, result, modules) { +function getEvaluated(output, modules) { try { var fn = vm.runInThisContext("(function(module, exports, require) {" + output + "})", "testcase.js"); var m = { exports: {}, id: 1 }; fn(m, m.exports, function(module) { - if(module === "./lib/css-base.js") + if(module === require.resolve("../lib/css-base")) return require("../lib/css-base"); if(module.indexOf("-!loader!") === 0) module = module.substr(9); @@ -27,7 +28,7 @@ function getEvaluated(output, result, modules) { } function assetEvaluated(output, result, modules) { - var exports = getEvaluated(output, result, modules); + var exports = getEvaluated(output, modules); exports.should.be.eql(result); } @@ -51,6 +52,26 @@ exports.test = function test(name, input, result, query, modules) { }); }; +exports.testLocals = function testLocals(name, input, result, query, modules) { + it(name, function() { + var output = cssLoaderLocals.call({ + options: { + context: "" + }, + loaders: [{request: "loader"}], + loaderIndex: 0, + context: "", + resource: "test.css", + request: "css-loader/locals!test.css", + query: query, + emitError: function(message) { + throw new Error(message); + } + }, input); + assetEvaluated(output, result, modules); + }); +}; + exports.testSingleItem = function testSingleItem(name, input, result, query, modules) { it(name, function() { var output = cssLoader.call({ @@ -67,10 +88,10 @@ exports.testSingleItem = function testSingleItem(name, input, result, query, mod throw new Error(message); } }, input); - var exports = getEvaluated(output, result, modules); + var exports = getEvaluated(output, modules); Array.isArray(exports).should.be.eql(true); (exports.length).should.be.eql(1); - (exports[0].length).should.be.eql(3); + (exports[0].length >= 3).should.be.eql(true); (exports[0][0]).should.be.eql(1); (exports[0][2]).should.be.eql(""); (exports[0][1]).should.be.eql(result); diff --git a/test/importTest.js b/test/importTest.js index f50d2724..d5417c87 100644 --- a/test/importTest.js +++ b/test/importTest.js @@ -5,20 +5,20 @@ var test = require("./helpers").test; describe("import", function() { test("import", "@import url(test.css);\n.class { a: b c d; }", [ [2, ".test{a: b}", ""], - [1, "\n.class { a: b c d; }", ""] + [1, ".class { a: b c d; }", ""] ], "", { "./test.css": [[2, ".test{a: b}", ""]] }); test("import 2", "@import url('test.css');\n.class { a: b c d; }", [ [2, ".test{a: b}", "screen"], - [1, "\n.class { a: b c d; }", ""] + [1, ".class { a: b c d; }", ""] ], "", { "./test.css": [[2, ".test{a: b}", "screen"]] }); test("import with media", "@import url('~test/css') screen and print;\n.class { a: b c d; }", [ [3, ".test{a: b}", "((min-width: 100px)) and (screen and print)"], [2, ".test{c: d}", "screen and print"], - [1, "\n.class { a: b c d; }", ""] + [1, ".class { a: b c d; }", ""] ], "", { "test/css": [ [3, ".test{a: b}", "(min-width: 100px)"], @@ -28,6 +28,6 @@ describe("import", function() { test("import external", "@import url(http://example.com/style.css);\n@import url(\"//example.com/style.css\");", [ [1, "@import url(http://example.com/style.css);", ""], [1, "@import url(//example.com/style.css);", ""], - [1, "\n", ""] + [1, "", ""] ]); }); diff --git a/test/localTest.js b/test/localTest.js index 1fef236f..e226506a 100644 --- a/test/localTest.js +++ b/test/localTest.js @@ -31,13 +31,13 @@ describe("local", function() { c7: "_c7" }, "?localIdentName=_[local]"); testLocal("comment in local", ":local(.c1/*.c2*/.c3) { background: red; }", [ - [1, "._c1/*.c2*/._c3 { background: red; }", ""] + [1, "._c1._c3 { background: red; }", ""] ], { c1: "_c1", c3: "_c3" }, "?localIdentName=_[local]"); testLocal("comment in local", ":local(.c1/*.c2*/.c3) { background: red; }", [ - [1, "._c1/*.c2*/._c3 { background: red; }", ""] + [1, "._c1._c3 { background: red; }", ""] ], { c1: "_c1", c3: "_c3" @@ -50,16 +50,16 @@ describe("local", function() { c4: "_c4" }, "?localIdentName=_[local]"); - testLocal("extends class simple", ":local(.c1) { a: 1; }\n:local(.c2) { extends: c1; b: 1; }", [ + testLocal("composes class simple", ":local(.c1) { a: 1; }\n:local(.c2) { composes: c1; b: 1; }", [ [1, "._c1 { a: 1; }\n._c2 { b: 1; }", ""] ], { c1: "_c1", c2: "_c2 _c1" }, "?localIdentName=_[local]"); - testLocal("extends class from module", [ - ":local(.c1) { extends: c2 from \"./module\"; b: 1; }", - ":local(.c3) { extends: c1; b: 3; }", - ":local(.c5) { extends: c2 c4 from \"./module\"; b: 5; }" + testLocal("composes class from module", [ + ":local(.c1) { composes: c2 from \"./module\"; b: 1; }", + ":local(.c3) { composes: c1; b: 3; }", + ":local(.c5) { composes: c2 c4 from \"./module\"; b: 5; }" ].join("\n"), [ [2, ".test{c: d}", ""], [1, [ @@ -83,12 +83,12 @@ describe("local", function() { return r; }()) }); - testLocal("extends class from module with import", [ + testLocal("composes class from module with import", [ "@import url(\"module\");", - ":local(.c1) { extends: c2 c3 from \"./module\"; extends: c4 from url(module); b: 1; }" + ":local(.c1) { composes: c2 c3 from \"./module\"; composes: c4 from \"./module\"; b: 1; }" ].join("\n"), [ [2, ".test{c: d}", ""], - [1, "\n._c1 { b: 1; }", ""] + [1, "._c1 { b: 1; }", ""] ], { c1: "_c1 imported-c2 imported-c3 imported-c4" }, "?localIdentName=_[local]", { diff --git a/test/localsTest.js b/test/localsTest.js new file mode 100644 index 00000000..be155f06 --- /dev/null +++ b/test/localsTest.js @@ -0,0 +1,36 @@ +/*globals describe */ + +var testLocals = require("./helpers").testLocals; + +describe("locals", function() { + testLocals("should return only locals", + ".abc :local(.def) { color: red; } :local .ghi .jkl { color: blue; }", + { + def: "_def", + ghi: "_ghi", + jkl: "_jkl" + }, + "?localIdentName=_[local]" + ); + testLocals("should return only locals with composing", + ":local(.abc) { color: red; } :local(.def) { composes: abc; background: green; }", + { + abc: "_abc", + def: "_def _abc" + }, + "?localIdentName=_[local]" + ); + testLocals("should return only locals with importing", + ":local(.abc) { composes: def from \"./module.css\"; }", + { + abc: "_abc imported_def imported_ghi" + }, + "?localIdentName=_[local]", + { + "./module.css": { + def: "imported_def imported_ghi", + ghi: "imported_ghi" + } + } + ); +}); diff --git a/test/moduleTest.js b/test/moduleTest.js index ce9ff7fb..b5a56aac 100644 --- a/test/moduleTest.js +++ b/test/moduleTest.js @@ -12,6 +12,6 @@ describe("module", function() { var source = fs.readFileSync(path.join(testCasesPath, name, "source.css"), "utf-8"); var expected = fs.readFileSync(path.join(testCasesPath, name, "expected.css"), "utf-8"); - test(name, source, expected, "?module&localIdentName=_[local]_"); + test(name, source, expected, "?module&sourceMap&localIdentName=_[local]_"); }); }); diff --git a/test/moduleTestCases/leak-scope/expected.css b/test/moduleTestCases/leak-scope/expected.css index d91d3f85..2d72df97 100644 --- a/test/moduleTestCases/leak-scope/expected.css +++ b/test/moduleTestCases/leak-scope/expected.css @@ -18,8 +18,8 @@ } ._c_ { - animation: c1; - animation: _c2_, c3, _c4_; + animation: _c1_; + animation: _c2_, _c3_, _c4_; } @keyframes d { @@ -29,7 +29,7 @@ .d1 { animation: d1; - animation: d2, _d3_, d4; + animation: d2, d3, d4; } .d2 { diff --git a/test/moduleTestCases/leak-scope/source.css b/test/moduleTestCases/leak-scope/source.css index 75060bc8..586ef5e7 100644 --- a/test/moduleTestCases/leak-scope/source.css +++ b/test/moduleTestCases/leak-scope/source.css @@ -12,24 +12,24 @@ animation: b; } -@keyframes :global c { +@keyframes :global(c) { 0% { left: 10px; } 100% { left: 20px; } } .c { - animation: :global c1; - animation: c2, :global c3, c4; + animation: c1; + animation: c2, c3, c4; } -@keyframes :global d { +@keyframes :global(d) { 0% { left: 10px; } 100% { left: 20px; } } :global .d1 { animation: d1; - animation: d2, :local(d3), d4; + animation: d2, d3, d4; } :global(.d2) { From 421c2d4cf51d1a077f4800a78157b27d7843e058 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 18 Jun 2015 16:08:30 +0200 Subject: [PATCH 2/6] added test case for minimized plus local --- test/localTest.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/localTest.js b/test/localTest.js index e226506a..52b095f5 100644 --- a/test/localTest.js +++ b/test/localTest.js @@ -1,12 +1,18 @@ /*globals describe */ var test = require("./helpers").test; +var testMinimize = require("./helpers").testMinimize; function testLocal(name, input, result, localsResult, query, modules) { result.locals = localsResult; test(name, input, result, query, modules); } +function testLocalMinimize(name, input, result, localsResult, query, modules) { + result.locals = localsResult; + testMinimize(name, input, result, query, modules); +} + describe("local", function() { testLocal("locals-format", ":local(.test) { background: red; }", [ [1, ".test-3tNsp { background: red; }", ""] @@ -22,6 +28,13 @@ describe("local", function() { someId: "_1j3LM6lKkKzRIt19ImYVnD", subClass: "_13LGdX8RMStbBE9w-t0gZ1" }); + testLocalMinimize("minimized plus local", ":local(.localClass) { background: red; }\n:local .otherClass { background: red; }\n:local(.empty) { }", [ + [1, "._localClass,._otherClass{background:red}", ""] + ], { + localClass: "_localClass", + otherClass: "_otherClass", + empty: "_empty" + }, "?localIdentName=_[local]"); testLocal("mode switching", ".c1 :local .c2 .c3 :global .c4 :local .c5, .c6 :local .c7 { background: red; }\n.c8 { background: red; }", [ [1, ".c1 ._c2 ._c3 .c4 ._c5, .c6 ._c7 { background: red; }\n.c8 { background: red; }", ""] ], { From 623b1977df367127bd0c7ea32655826d0997eb79 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 18 Jun 2015 16:11:33 +0200 Subject: [PATCH 3/6] add useful minimize options --- lib/processCss.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/processCss.js b/lib/processCss.js index 4f5e38df..6dbc5640 100644 --- a/lib/processCss.js +++ b/lib/processCss.js @@ -159,7 +159,10 @@ module.exports = function processCss(inputSource, inputMap, options) { ]); if(minimize) { - pipeline.use(cssnano()); + pipeline.use(cssnano({ + zindex: false, + urls: false + })); } var result = pipeline.process(inputSource, { From 927420e7852e2b450a7e1c79211cbb49ae846c35 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 18 Jun 2015 16:19:44 +0200 Subject: [PATCH 4/6] added import string test --- test/importTest.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/importTest.js b/test/importTest.js index d5417c87..6314fffa 100644 --- a/test/importTest.js +++ b/test/importTest.js @@ -9,6 +9,12 @@ describe("import", function() { ], "", { "./test.css": [[2, ".test{a: b}", ""]] }); + test("import with string", "@import \"test.css\";\n.class { a: b c d; }", [ + [2, ".test{a: b}", ""], + [1, ".class { a: b c d; }", ""] + ], "", { + "./test.css": [[2, ".test{a: b}", ""]] + }); test("import 2", "@import url('test.css');\n.class { a: b c d; }", [ [2, ".test{a: b}", "screen"], [1, ".class { a: b c d; }", ""] From e1c20e7043bd4cbb69ab55ee0edb1a25c5f40814 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 18 Jun 2015 16:23:50 +0200 Subject: [PATCH 5/6] added import var test case --- test/localTest.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/localTest.js b/test/localTest.js index 52b095f5..4e70e579 100644 --- a/test/localTest.js +++ b/test/localTest.js @@ -131,4 +131,16 @@ describe("local", function() { ], { "-a0-34a___f": "_1YJOcrkc6cyZmBAAvyPFOn" }, "?module"); + testLocal("imported values in decl", ".className { color: IMPORTED_NAME; }\n" + + ":import(\"./vars.css\") { IMPORTED_NAME: primary-color; }", [ + [1, "._className { color: red; }", ""] + ], { + "className": "_className" + }, "?module&localIdentName=_[local]", { + "./vars.css": { + locals: { + "primary-color": "red" + } + } + }); }); From 633cdeca883610958bb3b25f1ce6182d5d64c605 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 18 Jun 2015 16:30:06 +0200 Subject: [PATCH 6/6] move importPrefix into separate file allow to disable import prefix --- lib/getImportPrefix.js | 14 ++++++++++++++ lib/loader.js | 8 ++------ lib/localsLoader.js | 8 ++------ 3 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 lib/getImportPrefix.js diff --git a/lib/getImportPrefix.js b/lib/getImportPrefix.js new file mode 100644 index 00000000..5d3be772 --- /dev/null +++ b/lib/getImportPrefix.js @@ -0,0 +1,14 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ +module.exports = function getImportPrefix(loaderContext, query) { + if(query.importLoaders === false) + return ""; + var importLoaders = parseInt(query.importLoaders, 10) || 0; + var loadersRequest = loaderContext.loaders.slice( + loaderContext.loaderIndex, + loaderContext.loaderIndex + 1 + importLoaders + ).map(function(x) { return x.request; }).join("!"); + return "-!" + loadersRequest + "!"; +}; diff --git a/lib/loader.js b/lib/loader.js index 0e61d179..82dff7e6 100644 --- a/lib/loader.js +++ b/lib/loader.js @@ -5,13 +5,13 @@ var path = require("path"); var loaderUtils = require("loader-utils"); var processCss = require("./processCss"); +var getImportPrefix = require("./getImportPrefix"); module.exports = function(content, map) { if(this.cacheable) this.cacheable(); var query = loaderUtils.parseQuery(this.query); var root = query.root; - var importLoaders = parseInt(query.importLoaders, 10) || 0; var moduleMode = query.module; if(typeof map !== "string") { @@ -30,11 +30,7 @@ module.exports = function(content, map) { var cssAsString = JSON.stringify(result.source); // for importing CSS - var loadersRequest = this.loaders.slice( - this.loaderIndex, - this.loaderIndex + 1 + importLoaders - ).map(function(x) { return x.request; }).join("!"); - var importUrlPrefix = "-!" + loadersRequest + "!"; + var importUrlPrefix = getImportPrefix(this, query); var alreadyImported = {}; var importJs = result.importItems.filter(function(imp) { diff --git a/lib/localsLoader.js b/lib/localsLoader.js index ece70f65..50a2d4d6 100644 --- a/lib/localsLoader.js +++ b/lib/localsLoader.js @@ -4,12 +4,12 @@ */ var loaderUtils = require("loader-utils"); var processCss = require("./processCss"); +var getImportPrefix = require("./getImportPrefix"); module.exports = function(content) { if(this.cacheable) this.cacheable(); var query = loaderUtils.parseQuery(this.query); - var importLoaders = parseInt(query.importLoaders, 10) || 0; var moduleMode = query.module; var result = processCss(content, null, { @@ -20,11 +20,7 @@ module.exports = function(content) { }); // for importing CSS - var loadersRequest = this.loaders.slice( - this.loaderIndex, - this.loaderIndex + 1 + importLoaders - ).map(function(x) { return x.request; }).join("!"); - var importUrlPrefix = "-!" + loadersRequest + "!"; + var importUrlPrefix = getImportPrefix(this, query); function importItemMatcher(item) { var match = result.importItemRegExp.exec(item);