Repository: tivac/modular-css
Pull request #359 (2017-11-02 06:34:23)

feat: @value imports
As requested by @getkey in #354, you can now do `@value * from "./file.css"` and it'll import any `@value` definitions from `file.css` into the current file. Importantly, those definitions will also be **exported** from the current file, so you can use this feature as a way to build layered theming.

Local `@value`s will override any imported `@value`s with the same name, so that should work as expected. My test case is pretty simple but I think it covers the main points.

Here's the example from the docs:

### Importing

You can also import all the `@value` definitions from another file into the current one. The imported `@value`s will also be exported from that file, allowing for simple theming setups with overriden colors/dimensions/etc.

```css
/* colors.css */
@value main: red;
@value bg: white;
```

```css
/* mobile-colors.css */
@value * from "./colors.css";
@value bg: gray;
```

```css
@value * as colors from "./mobile-colors.css";

body {
    background: colors.bg; /* gray */
    color: colors.main; /* red */
}
```

getkey: Looks very cool!

Did you consider supporting a syntax that mirrors the following, too?

```css
@value one, two, three, red, blue, green, small, medium, large from "./constants.css";
```

I can't think of a use case for it on the top of my head, but I think it would be better for orthogonality.
tivac: @getkey you can already do that. Imported named values were always part of the exports of a file, this just needed to add the `*` shorthand. That isn't documented though, so I've opened #360 to make sure it happens.

[example](http://m-css.com/#W3sibmFtZSI6Ii8xLmNzcyIsImNzcyI6IkB2YWx1ZSBhOiByZWQ7XG5AdmFsdWUgYjogYmx1ZTtcbkB2YWx1ZSBjOiBibGFjaztcbiJ9LHsibmFtZSI6Ii8yLmNzcyIsImNzcyI6IkB2YWx1ZSBhLCBiIGZyb20gXCIuLzEuY3NzXCI7XG5AdmFsdWUgYzogeWVsbG93OyJ9LHsibmFtZSI6Ii8zLmNzcyIsImNzcyI6IkB2YWx1ZSBhLCBiLCBjIGZyb20gXCIvMi5jc3NcIjtcblxuLmZvb2dhIHtcbiAgY29sb3I6IGE7XG4gIGJhY2tncm91bmQ6IGI7XG4gIGJvcmRlci1jb2xvcjogYztcbn0ifV0=)
getkey: Awesome! 👍
tivac: Need to figure out the CI failures, this seems fine though.

Patch:
diff --git a/docs/features.md b/docs/features.md
index b13d5a19..b6243dc5 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -4,6 +4,7 @@ CSS Modules defines a bunch of great features, and `modular-css` supports the be
 
 - [Values](#values)
   - [Namespaces](#namespaces)
+  - [Importing](#importing)
 - [Scoped Selectors](#scoped-selectors)
 - [Composition](#composition)
   - [Overriding Styles](#overriding-styles)
@@ -48,6 +49,31 @@ Values are useful in CSS, they're coming to the spec soon. Use them now because
 }
 ```
 
+### Importing
+
+You can also import all the `@value` definitions from another file into the current one. The imported `@value`s will also be exported from that file, allowing for simple theming setups with overriden colors/dimensions/etc.
+
+```css
+/* colors.css */
+@value main: red;
+@value bg: white;
+```
+
+```css
+/* mobile-colors.css */
+@value * from "./colors.css";
+@value bg: gray;
+```
+
+```css
+@value * as colors from "./mobile-colors.css";
+
+body {
+    background: colors.bg; /* gray */
+    color: colors.main; /* red */
+}
+```
+
 ## Scoped Selectors
 
 By default all CSS selectors live in the global scope of the page and are chosen based on specificity rules. This has proven to be a model that makes it difficult to succeed and incredibly easy to blow off your own foot. `modular-css` scopes all selectors to the local file by default, ensuring that your CSS is always exactly as specific as it should be.
diff --git a/packages/core/parsers/parser.js b/packages/core/parsers/parser.js
index 91c5292d..f27b7493 100644
--- a/packages/core/parsers/parser.js
+++ b/packages/core/parsers/parser.js
@@ -177,42 +177,48 @@ function peg$parse(input, options) {
           },
       peg$c32 = "*",
       peg$c33 = peg$literalExpectation("*", false),
-      peg$c34 = "as",
-      peg$c35 = peg$literalExpectation("as", false),
-      peg$c36 = function(name, source) {
+      peg$c34 = function(source) {
               return {
-                  type   : "namespace",
+                  type : "import",
+                  source
+              };
+          },
+      peg$c35 = "as",
+      peg$c36 = peg$literalExpectation("as", false),
+      peg$c37 = function(name, source) {
+              return {
+                  type : "namespace",
                   source,
                   name
               };
           },
-      peg$c37 = ":",
-      peg$c38 = peg$literalExpectation(":", false),
-      peg$c39 = peg$anyExpectation(),
-      peg$c40 = function(ref, value) {
+      peg$c38 = ":",
+      peg$c39 = peg$literalExpectation(":", false),
+      peg$c40 = peg$anyExpectation(),
+      peg$c41 = function(ref, value) {
               return {
                   type  : "assignment",
                   name  : ref.name,
                   value : value.join("")
               };
           },
-      peg$c41 = function(refs, source) {
+      peg$c42 = function(refs, source) {
               return {
-                  type   : "composition",
+                  type : "composition",
                   source,
                   refs
               };
           },
-      peg$c42 = ".",
-      peg$c43 = peg$literalExpectation(".", false),
-      peg$c44 = function(ns, ref) {
+      peg$c43 = ".",
+      peg$c44 = peg$literalExpectation(".", false),
+      peg$c45 = function(ns, ref) {
               return {
                   type : "namespaced",
                   ns,
                   ref
               };
           },
-      peg$c45 = function(refs) {
+      peg$c46 = function(refs) {
               return {
                   type : "simple",
                   refs
@@ -358,15 +364,18 @@ function peg$parse(input, options) {
   function peg$parsestart() {
     var s0;
 
-    s0 = peg$parsecreate_namespace();
+    s0 = peg$parseimport_namespace();
     if (s0 === peg$FAILED) {
-      s0 = peg$parseassignment();
+      s0 = peg$parsecreate_namespace();
       if (s0 === peg$FAILED) {
-        s0 = peg$parsecomposition();
+        s0 = peg$parseassignment();
         if (s0 === peg$FAILED) {
-          s0 = peg$parsenamespaced();
+          s0 = peg$parsecomposition();
           if (s0 === peg$FAILED) {
-            s0 = peg$parsesimple();
+            s0 = peg$parsenamespaced();
+            if (s0 === peg$FAILED) {
+              s0 = peg$parsesimple();
+            }
           }
         }
       }
@@ -799,6 +808,47 @@ function peg$parse(input, options) {
     return s0;
   }
 
+  function peg$parseimport_namespace() {
+    var s0, s1, s2, s3, s4;
+
+    s0 = peg$currPos;
+    s1 = peg$parse_();
+    if (s1 !== peg$FAILED) {
+      if (input.charCodeAt(peg$currPos) === 42) {
+        s2 = peg$c32;
+        peg$currPos++;
+      } else {
+        s2 = peg$FAILED;
+        if (peg$silentFails === 0) { peg$fail(peg$c33); }
+      }
+      if (s2 !== peg$FAILED) {
+        s3 = peg$parse_();
+        if (s3 !== peg$FAILED) {
+          s4 = peg$parsefrom_source();
+          if (s4 !== peg$FAILED) {
+            peg$savedPos = s0;
+            s1 = peg$c34(s4);
+            s0 = s1;
+          } else {
+            peg$currPos = s0;
+            s0 = peg$FAILED;
+          }
+        } else {
+          peg$currPos = s0;
+          s0 = peg$FAILED;
+        }
+      } else {
+        peg$currPos = s0;
+        s0 = peg$FAILED;
+      }
+    } else {
+      peg$currPos = s0;
+      s0 = peg$FAILED;
+    }
+
+    return s0;
+  }
+
   function peg$parsecreate_namespace() {
     var s0, s1, s2, s3, s4, s5, s6, s7;
 
@@ -815,12 +865,12 @@ function peg$parse(input, options) {
       if (s2 !== peg$FAILED) {
         s3 = peg$parse_();
         if (s3 !== peg$FAILED) {
-          if (input.substr(peg$currPos, 2) === peg$c34) {
-            s4 = peg$c34;
+          if (input.substr(peg$currPos, 2) === peg$c35) {
+            s4 = peg$c35;
             peg$currPos += 2;
           } else {
             s4 = peg$FAILED;
-            if (peg$silentFails === 0) { peg$fail(peg$c35); }
+            if (peg$silentFails === 0) { peg$fail(peg$c36); }
           }
           if (s4 !== peg$FAILED) {
             s5 = peg$parse_();
@@ -830,7 +880,7 @@ function peg$parse(input, options) {
                 s7 = peg$parsefrom_source();
                 if (s7 !== peg$FAILED) {
                   peg$savedPos = s0;
-                  s1 = peg$c36(s6, s7);
+                  s1 = peg$c37(s6, s7);
                   s0 = s1;
                 } else {
                   peg$currPos = s0;
@@ -873,11 +923,11 @@ function peg$parse(input, options) {
       s2 = peg$parse_();
       if (s2 !== peg$FAILED) {
         if (input.charCodeAt(peg$currPos) === 58) {
-          s3 = peg$c37;
+          s3 = peg$c38;
           peg$currPos++;
         } else {
           s3 = peg$FAILED;
-          if (peg$silentFails === 0) { peg$fail(peg$c38); }
+          if (peg$silentFails === 0) { peg$fail(peg$c39); }
         }
         if (s3 !== peg$FAILED) {
           s4 = peg$parse_();
@@ -888,7 +938,7 @@ function peg$parse(input, options) {
               peg$currPos++;
             } else {
               s6 = peg$FAILED;
-              if (peg$silentFails === 0) { peg$fail(peg$c39); }
+              if (peg$silentFails === 0) { peg$fail(peg$c40); }
             }
             if (s6 !== peg$FAILED) {
               while (s6 !== peg$FAILED) {
@@ -898,7 +948,7 @@ function peg$parse(input, options) {
                   peg$currPos++;
                 } else {
                   s6 = peg$FAILED;
-                  if (peg$silentFails === 0) { peg$fail(peg$c39); }
+                  if (peg$silentFails === 0) { peg$fail(peg$c40); }
                 }
               }
             } else {
@@ -906,7 +956,7 @@ function peg$parse(input, options) {
             }
             if (s5 !== peg$FAILED) {
               peg$savedPos = s0;
-              s1 = peg$c40(s1, s5);
+              s1 = peg$c41(s1, s5);
               s0 = s1;
             } else {
               peg$currPos = s0;
@@ -941,7 +991,7 @@ function peg$parse(input, options) {
       s2 = peg$parsefrom_source();
       if (s2 !== peg$FAILED) {
         peg$savedPos = s0;
-        s1 = peg$c41(s1, s2);
+        s1 = peg$c42(s1, s2);
         s0 = s1;
       } else {
         peg$currPos = s0;
@@ -962,17 +1012,17 @@ function peg$parse(input, options) {
     s1 = peg$parseident();
     if (s1 !== peg$FAILED) {
       if (input.charCodeAt(peg$currPos) === 46) {
-        s2 = peg$c42;
+        s2 = peg$c43;
         peg$currPos++;
       } else {
         s2 = peg$FAILED;
-        if (peg$silentFails === 0) { peg$fail(peg$c43); }
+        if (peg$silentFails === 0) { peg$fail(peg$c44); }
       }
       if (s2 !== peg$FAILED) {
         s3 = peg$parseident();
         if (s3 !== peg$FAILED) {
           peg$savedPos = s0;
-          s1 = peg$c44(s1, s3);
+          s1 = peg$c45(s1, s3);
           s0 = s1;
         } else {
           peg$currPos = s0;
@@ -997,7 +1047,7 @@ function peg$parse(input, options) {
     s1 = peg$parsereferences();
     if (s1 !== peg$FAILED) {
       peg$savedPos = s0;
-      s1 = peg$c45(s1);
+      s1 = peg$c46(s1);
     }
     s0 = s1;
 
diff --git a/packages/core/parsers/parser.pegjs b/packages/core/parsers/parser.pegjs
index 61614bd6..c274fa87 100644
--- a/packages/core/parsers/parser.pegjs
+++ b/packages/core/parsers/parser.pegjs
@@ -1,5 +1,6 @@
 start
-    = create_namespace
+    = import_namespace
+    / create_namespace
     / assignment
     / composition
     / namespaced
@@ -44,11 +45,20 @@ from_source
 
 // @value only
 
+// * from "./wooga"
+import_namespace
+    = _ "*" _ source:from_source {
+        return {
+            type : "import",
+            source
+        };
+    }
+
 // * as fooga from "./wooga"
 create_namespace
     = _ "*" _ "as" _ name:ident source:from_source {
         return {
-            type   : "namespace",
+            type : "namespace",
             source,
             name
         };
@@ -71,7 +81,7 @@ assignment
 composition
     = refs:references source:from_source {
         return {
-            type   : "composition",
+            type : "composition",
             source,
             refs
         };
diff --git a/packages/core/plugins/values-export.js b/packages/core/plugins/values-export.js
index fc2d8007..a7b6fab4 100644
--- a/packages/core/plugins/values-export.js
+++ b/packages/core/plugins/values-export.js
@@ -1,15 +1,20 @@
 "use strict";
 
-var search = /values-local|values-composed/;
+var order = [
+        "modular-css-values-imported",
+        "modular-css-values-local",
+        "modular-css-values-composed"
+    ];
 
 // Find messages from other value plugins & save them to files object
 module.exports = (css, result) => {
     var file   = result.opts.files[result.opts.from],
         values = result.messages
-            .filter((msg) => msg.plugin && msg.plugin.search(search) > -1)
+            .filter((msg) => msg.plugin && order.indexOf(msg.plugin) > -1)
+            .sort((a, b) => order.indexOf(a.plugin) - order.indexOf(b.plugin))
             .reduce((prev, curr) => Object.assign(prev, curr.values), Object.create(null));
     
-    file.values = Object.assign(file.values || Object.create(null), values);
+    file.values = Object.assign(Object.create(null), values, file.values || {});
 };
 
 module.exports.postcssPlugin = "modular-css-values-export";
diff --git a/packages/core/plugins/values-imported.js b/packages/core/plugins/values-imported.js
new file mode 100644
index 00000000..86e9495b
--- /dev/null
+++ b/packages/core/plugins/values-imported.js
@@ -0,0 +1,38 @@
+"use strict";
+
+var parser  = require("../parsers/parser.js"),
+    
+    plugin = "modular-css-values-imported";
+
+// Find @value * from "./wooga.css" entries & catalog/remove them
+module.exports = (css, result) => {
+    var values = Object.create(null);
+
+    css.walkAtRules("value", (rule) => {
+        var parsed = parser.parse(rule.params),
+            source;
+        
+        /* istanbul ignore if */
+        if(parsed.type !== "import") {
+            return;
+        }
+
+        source = result.opts.files[
+            result.opts.resolve(result.opts.from, parsed.source)
+        ];
+
+        values = Object.assign(values, source.values);
+
+        rule.remove();
+    });
+    
+    if(Object.keys(values).length > 0) {
+        result.messages.push({
+            type : "modular-css",
+            plugin,
+            values
+        });
+    }
+};
+
+module.exports.postcssPlugin = plugin;
diff --git a/packages/core/plugins/values-namespaced.js b/packages/core/plugins/values-namespaced.js
index 7381d333..cf8b44f8 100644
--- a/packages/core/plugins/values-namespaced.js
+++ b/packages/core/plugins/values-namespaced.js
@@ -4,7 +4,7 @@ var parser  = require("../parsers/parser.js"),
     
     plugin = "modular-css-values-namespaced";
 
-// Find @value fooga: wooga entries & catalog/remove them
+// Find @value * as fooga from "./wooga.css" entries & catalog/remove them
 module.exports = (css, result) => {
     var values = Object.create(null);
 
diff --git a/packages/core/processor.js b/packages/core/processor.js
index 2f9eeb6a..ebad1696 100644
--- a/packages/core/processor.js
+++ b/packages/core/processor.js
@@ -76,6 +76,7 @@ function Processor(opts) {
 
     this._process = postcss([
         require("./plugins/values-composed.js"),
+        require("./plugins/values-imported.js"),
         require("./plugins/values-export.js"),
         require("./plugins/values-namespaced.js"),
         require("./plugins/values-replace.js"),


Test patch:
diff --git a/packages/cli/test/cli.test.js b/packages/cli/test/cli.test.js
index 7d1050d9..2b08c9b9 100644
--- a/packages/cli/test/cli.test.js
+++ b/packages/cli/test/cli.test.js
@@ -8,59 +8,61 @@ var tester = require("cli-tester/es5"),
 
 function success(out) {
     expect(out.code).toBe(0);
-
+    
     return out;
 }
-
+    
 describe("/cli.js", function() {
+    // Since these tests keep failing on Travis...
+    jest.setTimeout(10000);
+    
     afterAll(() => require("shelljs").rm("-rf", "./packages/cli/test/output"));
 
-    it("should show help with no args", function() {
-        return tester(
+    it("should show help with no args", () =>
+        tester(
             cli
         )
         .then(success)
-        .then((out) => expect(out.stdout).toMatchSnapshot());
-    });
+        .then((out) => expect(out.stdout).toMatchSnapshot())
+    );
 
-    it("should default to outputting to stdout", function() {
-        return tester(
+    it("should default to outputting to stdout", () =>
+        tester(
             cli,
             "./packages/cli/test/specimens/simple.css"
         )
         .then(success)
-        .then((out) => expect(out.stdout).toMatchSnapshot());
-    });
+        .then((out) => expect(out.stdout).toMatchSnapshot())
+    );
 
-    it("should support outputting to a file", function() {
-        return tester(
+    it("should support outputting to a file", () =>
+        tester(
             cli,
             "--out=./packages/cli/test/output/simple.css",
             "./packages/cli/test/specimens/simple.css"
         )
         .then(success)
-        .then(() => expect(read("simple.css")).toMatchSnapshot());
-    });
+        .then(() => expect(read("simple.css")).toMatchSnapshot())
+    );
 
-    it("should support outputting compositions to a file", function() {
-        return tester(
+    it("should support outputting compositions to a file", () =>
+        tester(
             cli,
             "--json=./packages/cli/test/output/classes.json",
             "./packages/cli/test/specimens/simple.css"
         )
         .then(success)
-        .then(() => expect(read("classes.json")).toMatchSnapshot());
-    });
+        .then(() => expect(read("classes.json")).toMatchSnapshot())
+    );
 
-    it("should return the correct error code on invalid CSS", function() {
-        return tester(
+    it("should return the correct error code on invalid CSS", () =>
+        tester(
             cli,
             "./packages/cli/test/specimens/invalid.css"
         )
         .then((out) => {
             expect(out.code).toBe(1);
-            
             expect(out.stderr).toMatch("Invalid composes reference");
-        });
-    });
+        })
+    );
 });
diff --git a/packages/core/test/__snapshots__/values.test.js.snap b/packages/core/test/__snapshots__/values.test.js.snap
index 659555d1..284d1b4f 100644
--- a/packages/core/test/__snapshots__/values.test.js.snap
+++ b/packages/core/test/__snapshots__/values.test.js.snap
@@ -1,5 +1,41 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`/processor.js values should support exporting imported variables 1`] = `
+"/* packages/core/test/specimens/values.css */
+
+/* packages/core/test/specimens/value-import.css */
+.red {
+    color: red;
+}
+
+.green {
+    color: green;
+}
+
+/* packages/core/test/specimens/value-export.css */
+.red {
+    color: red;
+}
+
+.green {
+    color: green;
+}
+"
+`;
+
+exports[`/processor.js values should support importing variables from a file 1`] = `
+"/* packages/core/test/specimens/values.css */
+/* packages/core/test/specimens/value-import.css */
+.red {
+    color: red;
+}
+
+.green {
+    color: green;
+}
+"
+`;
+
 exports[`/processor.js values should support local values in value composition 1`] = `
 Object {
   "fooga": Array [
diff --git a/packages/core/test/specimens/value-export.css b/packages/core/test/specimens/value-export.css
new file mode 100644
index 00000000..95742871
--- /dev/null
+++ b/packages/core/test/specimens/value-export.css
@@ -0,0 +1,9 @@
+@value * as colors from "./value-import.css";
+
+.red {
+    color: colors.a;
+}
+
+.green {
+    color: colors.b;
+}
diff --git a/packages/core/test/specimens/value-import.css b/packages/core/test/specimens/value-import.css
new file mode 100644
index 00000000..31f4b039
--- /dev/null
+++ b/packages/core/test/specimens/value-import.css
@@ -0,0 +1,10 @@
+@value * from "./values.css";
+@value b: green;
+
+.red {
+    color: a;
+}
+
+.green {
+    color: b;
+}
diff --git a/packages/core/test/values.test.js b/packages/core/test/values.test.js
index 8e9f987c..8a16d1d9 100644
--- a/packages/core/test/values.test.js
+++ b/packages/core/test/values.test.js
@@ -74,6 +74,22 @@ describe("/processor.js", () => {
             )
         );
 
+        it("should support importing variables from a file", () =>
+            processor.file(require.resolve("./specimens/value-import.css"))
+            .then(() => processor.output())
+            .then((result) =>
+                expect(result.css).toMatchSnapshot()
+            )
+        );
+
+        it("should support exporting imported variables", () =>
+            processor.file(require.resolve("./specimens/value-export.css"))
+            .then(() => processor.output())
+            .then((result) =>
+                expect(result.css).toMatchSnapshot()
+            )
+        );
+
         it("should support value composition", () =>
             processor.file(require.resolve("./specimens/value-composition.css"))
             .then(() => processor.output())