diff --git a/README.md b/README.md index 57fdccc..02a672d 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,11 @@ increase. Depending on how you use it, Optimus: - concatenates your JavaScript and CSS files into bundles. -- minifies your JavaScript with [UglifyJS 2](https://github.com/mishoo/UglifyJS2) -- minifies your CSS with [CSSO](http://bem.info/tools/optimizers/csso/) - adds cache-busters to your static asset URLs - adds [far future Expires headers](http://developer.yahoo.com/performance/rules.html#expires) +- minifies your JavaScript with [UglifyJS 2](https://github.com/mishoo/UglifyJS2) +- minifies your CSS with [CSSO](http://bem.info/tools/optimizers/csso/) +- inlines CSS imports while preserving media queries You might also be interested in: @@ -213,6 +214,7 @@ If you want to mix and match optimizations, here's how you do that: (-> assets (optimizations/minify-js-assets options) (optimizations/minify-css-assets options) + (optimizations/inline-css-imports) (optimizations/concatenate-bundles) (optimizations/add-cache-busted-expires-headers) (optimizations/add-last-modified-headers))) diff --git a/src/optimus/assets/load_css.clj b/src/optimus/assets/load_css.clj index fba66ae..586bcf3 100644 --- a/src/optimus/assets/load_css.clj +++ b/src/optimus/assets/load_css.clj @@ -21,7 +21,7 @@ (defn- data-url? [#^String url] (.startsWith url "data:")) -(defn- external-url? [#^String url] +(defn external-url? [#^String url] (re-matches #"^(?://|http://|https://).*" url)) (defn- url-match [[match & urls]] @@ -48,13 +48,20 @@ (map url-match) (map remove-url-appendages) (remove data-url?) - (remove external-url?))) + (remove external-url?) + (set))) + +(defn update-css-references [asset] + (let [paths (paths-in-css asset)] + (if (empty? paths) + (dissoc asset :references) + (assoc asset :references paths)))) (defn create-css-asset [path contents last-modified] - (let [asset (-> (create-asset path contents - :last-modified last-modified) - (make-css-urls-absolute))] - (assoc asset :references (set (paths-in-css asset))))) + (-> (create-asset path contents + :last-modified last-modified) + (make-css-urls-absolute) + (update-css-references))) (defn load-css-asset [public-dir path] (let [resource (existing-resource public-dir path)] diff --git a/src/optimus/optimizations.clj b/src/optimus/optimizations.clj index c79c96a..b717722 100644 --- a/src/optimus/optimizations.clj +++ b/src/optimus/optimizations.clj @@ -2,18 +2,21 @@ (:require [optimus.optimizations.add-cache-busted-expires-headers] [optimus.optimizations.concatenate-bundles] [optimus.optimizations.minify] - [optimus.optimizations.add-last-modified-headers])) + [optimus.optimizations.add-last-modified-headers] + [optimus.optimizations.inline-css-imports])) (def minify-js-assets optimus.optimizations.minify/minify-js-assets) (def minify-css-assets optimus.optimizations.minify/minify-css-assets) (def concatenate-bundles optimus.optimizations.concatenate-bundles/concatenate-bundles) (def add-cache-busted-expires-headers optimus.optimizations.add-cache-busted-expires-headers/add-cache-busted-expires-headers) (def add-last-modified-headers optimus.optimizations.add-last-modified-headers/add-last-modified-headers) +(def inline-css-imports optimus.optimizations.inline-css-imports/inline-css-imports) (defn all [assets options] (-> assets (minify-js-assets options) (minify-css-assets options) + (inline-css-imports) (concatenate-bundles) (add-cache-busted-expires-headers) (add-last-modified-headers))) diff --git a/src/optimus/optimizations/inline_css_imports.clj b/src/optimus/optimizations/inline_css_imports.clj new file mode 100644 index 0000000..7b9be60 --- /dev/null +++ b/src/optimus/optimizations/inline_css_imports.clj @@ -0,0 +1,29 @@ +(ns optimus.optimizations.inline-css-imports + (:require [clojure.string :as str] + [optimus.assets.load-css :refer [update-css-references external-url?]])) + +(def import-re #"@import (?:url)?[('\"]{1,2}([^']+?)[)'\"]{1,2} ?([^;]*);") + +(defn- by-path [path assets] + (first (filter #(= path (:path %)) assets))) + +(defn- inline-import-match [asset assets [match path media]] + (if (external-url? path) + (throw (Exception. "Import of external URL http://external.css in /main.css is strongly adviced against. It's a performance killer. In fact, there's no option to allow this. Use a link in your HTML instead. Open an issue if you really, really need it.")) + (let [contents (:contents (by-path path assets))] + (if (empty? media) + contents + (str "@media " media " { " contents " }"))))) + +(defn- is-css [#^String path] + (.endsWith path ".css")) + +(defn- inline-css-imports-1 [asset assets] + (if-not (is-css (:path asset)) + asset + (-> asset + (assoc :contents (str/replace (:contents asset) import-re (partial inline-import-match asset assets))) + (update-css-references)))) + +(defn inline-css-imports [assets] + (map #(inline-css-imports-1 % assets) assets)) diff --git a/test/optimus/assets_test.clj b/test/optimus/assets_test.clj index 510772c..0ecdce0 100644 --- a/test/optimus/assets_test.clj +++ b/test/optimus/assets_test.clj @@ -106,7 +106,7 @@ ["/other.css" "#id {}"]] (->> (load-assets public-dir ["/main.css"]) (map #(select-keys % #{:path :references}))) => [{:path "/main.css" :references #{"/other.css"}} - {:path "/other.css" :references #{}}])) + {:path "/other.css"}])) (with-files [["/main.css" "#id { background: url(data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)}"]] (fact diff --git a/test/optimus/optimizations/inline_css_imports_test.clj b/test/optimus/optimizations/inline_css_imports_test.clj new file mode 100644 index 0000000..e3ec83e --- /dev/null +++ b/test/optimus/optimizations/inline_css_imports_test.clj @@ -0,0 +1,46 @@ +(ns optimus.optimizations.inline-css-imports-test + (:require [optimus.optimizations.inline-css-imports :refer :all] + [midje.sweet :refer :all])) + +(fact + "CSS imports are inlined from the asset list, and removed from the + references." + + (inline-css-imports [{:path "/main.css" :contents "@import '/other.css'; #id {}" :references #{"/other.css"}} + {:path "/other.css" :contents ".class {}"}]) + => [{:path "/main.css" :contents ".class {} #id {}"} + {:path "/other.css" :contents ".class {}"}]) + +(fact + "CSS imports can take many forms." + + (let [assets [{:path "/main.css" :contents "@import '/other.css';"} + {:path "/other.css" :contents ".class {}"}] + expected [{:path "/main.css" :contents ".class {}"} + {:path "/other.css" :contents ".class {}"}]] + + (inline-css-imports (assoc-in assets [0 :contents] "@import '/other.css';")) => expected + (inline-css-imports (assoc-in assets [0 :contents] "@import \"/other.css\";")) => expected + (inline-css-imports (assoc-in assets [0 :contents] "@import url('/other.css');")) => expected + (inline-css-imports (assoc-in assets [0 :contents] "@import url(\"/other.css\");")) => expected + (inline-css-imports (assoc-in assets [0 :contents] "@import url(/other.css);")) => expected)) + +(fact + "Media queries are conserved." + + (inline-css-imports [{:path "/main.css" :contents "@import '/other.css' screen and (orientation:landscape);"} + {:path "/other.css" :contents ".class {}"}]) + => [{:path "/main.css" :contents "@media screen and (orientation:landscape) { .class {} }"} + {:path "/other.css" :contents ".class {}"}]) + +(fact + "External URLs for @imports are not tolerated. It's disastrous for frontend performance." + + (inline-css-imports [{:path "/main.css" :contents "@import 'http://external.css';"}]) + => (throws Exception "Import of external URL http://external.css in /main.css is strongly adviced against. It's a performance killer. In fact, there's no option to allow this. Use a link in your HTML instead. Open an issue if you really, really need it.")) + +(fact + "Non-CSS assets are left to their own devices." + + (inline-css-imports [{:path "/main.js" :contents "@import '/other.css';"}]) + => [{:path "/main.js" :contents "@import '/other.css';"}]) diff --git a/test/optimus/optimizations/minify_test.clj b/test/optimus/optimizations/minify_test.clj index ae205f3..603adcf 100644 --- a/test/optimus/optimizations/minify_test.clj +++ b/test/optimus/optimizations/minify_test.clj @@ -72,7 +72,6 @@ (fact (minify-css "body {\n color: red;\n}") => "body{color:red}") (fact (minify-css "body {\n color: red") => (throws Exception "Please check the validity of the CSS block starting from the line #1")) - (fact "You can turn off structural optimizations." @@ -82,6 +81,17 @@ (minify-css "body { color: red; } body { font-size: 10px; }" {:optimize-css-structure false}) => "body{color:red}body{font-size:10px}") +(fact + "CSSO doesn't mess up percentages after rgb-colors. It used to tho. + Don't want any regressions." + + (minify-css "body { background: -webkit-linear-gradient(bottom, rgb(209,209,209) 10%, rgb(250,250,250) 55%);}") + => "body{background:-webkit-linear-gradient(bottom,#d1d1d1 10%,#fafafa 55%)}") + +#_(fact + "CSSO doesn't mess up media queries." ;; well, it does now, if you have multiple and statements. + (minify-css "@media screen and (orientation:landscape) {#id{color:red}}") => "@media screen and (orientation:landscape){#id{color:red}}" + (minify-css "@import 'abc.css' screen and (min-width:7) and (max-width:9);") => "@import 'abc.css' screen and (min-width:7) and (max-width:9);") (fact "It minifies a list of CSS assets."