Skip to content
Permalink
Browse files

feat: composes at-rule (#635)

Fixes #633 

Adds a new `@COMPOSES` at-rule that can be used to include all of the exported classes/compositions from one other file.
  • Loading branch information...
tivac committed Jul 6, 2019
1 parent 9f6fb0d commit 27d696a77036a88e58a3255998ef8684c6044849
@@ -0,0 +1,49 @@
"use strict";

const parser = require("../parsers/at-composes.js");

const plugin = "modular-css-at-composes";

module.exports = (css, { opts, messages }) => {
const { files, resolve, from } = opts;

let source;

css.walkAtRules("composes", (rule) => {
if(source) {
throw rule.error(`Only one @composes rule per file`, { word : "composes" });
}

// We know it's safe, otherwise it would have failed in graph-nodes pass
const parsed = parser.parse(rule.params);

source = files[resolve(from, parsed.source)];

// Remove the @composes from the output
rule.remove();
});

if(!source) {
return;
}

// Need to separate out just the classes, values are handled via namespaced imports
const atcomposes = Object.create(null);

for(const [ key, value ] of Object.entries(source.exports)) {
if(key in source.values) {
continue;
}

atcomposes[key] = value;
}

messages.push({
type : "modular-css",
plugin,

atcomposes,
});
};

module.exports.postcssPlugin = plugin;
@@ -30,10 +30,13 @@ const composesFirst = (decl) => {
module.exports = (css, result) => {
const { opts } = result;

const refs = message(result, "classes");
const refs = {
...message(result, "atcomposes"),
...message(result, "classes"),
};

const map = invert(refs);

const out = Object.assign(Object.create(null), refs);
const out = { ...refs };
const graph = new Graph();

Object.keys(refs).forEach((key) => graph.addNode(key));
@@ -2,9 +2,10 @@

const selector = require("postcss-selector-parser");

const values = require("../parsers/values.js");
const atcomposes = require("../parsers/at-composes.js");
const composes = require("../parsers/composes.js");
const external = require("../parsers/external.js");
const values = require("../parsers/values.js");

const plugin = "modular-css-graph-nodes";

@@ -57,6 +58,9 @@ module.exports = (css, result) => {

// @value <value> from <file>
css.walkAtRules("value", (rule) => parse(values, rule, rule.params));

// @composes <file>
css.walkAtRules("composes", (rule) => parse(atcomposes, rule, rule.params));

// { composes: <rule> from <file> }
css.walkDecls("composes", (rule) => parse(composes, rule, rule.value));
@@ -86,6 +86,7 @@ class Processor {
]);

this._process = postcss([
require("./plugins/at-composes.js"),
require("./plugins/values-composed.js"),
require("./plugins/values-imported.js"),
require("./plugins/values-export.js"),
@@ -304,7 +305,7 @@ class Processor {
);

Object.defineProperty(result, "compositions", {
get : () => output.compositions(this)
get : () => output.compositions(this),
});

return result;
@@ -0,0 +1,83 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`/processor.js @composes should allow composing classes from the composed file 1`] = `
Object {
"a": Array [
"wooga",
"a",
],
"b": Array [
"b",
],
"wooga": Array [
"wooga",
],
}
`;

exports[`/processor.js @composes should allow for chains of @composes-included files 1`] = `
Object {
"a": Array [
"wooga",
"notblue",
"a",
],
"b": Array [
"wooga",
"b",
],
"notblue": Array [
"wooga",
"notblue",
],
"wooga": Array [
"wooga",
],
}
`;

exports[`/processor.js @composes should include compositions from the composed file 1`] = `
Object {
"packages/processor/test/specimens/at-composes.css": Object {
"a": "a",
"b": "wooga b",
"wooga": "wooga",
},
"packages/processor/test/specimens/simple.css": Object {
"wooga": "wooga",
},
}
`;

exports[`/processor.js @composes should include exported classes from the composed file 1`] = `
Object {
"a": Array [
"a",
],
"b": Array [
"b",
],
"booga": Array [
"booga",
],
"looga": Array [
"booga",
"looga",
],
}
`;

exports[`/processor.js @composes should not include exported values from the composed file 1`] = `Object {}`;

exports[`/processor.js @composes should output css from the composed file 1`] = `
"/* packages/processor/test/specimens/simple.css */
.wooga { color: red; }
/* packages/processor/test/specimens/at-composes.css */
.a {
color: aqua;
}
.b {
color: blue;
}
"
`;
@@ -0,0 +1,134 @@
"use strict";

const dedent = require("dedent");

const namer = require("@modular-css/test-utils/namer.js");
const Processor = require("../processor.js");

const id = "./packages/processor/test/specimens/at-composes.css";

describe("/processor.js", () => {
describe("@composes", () => {
let processor;

beforeEach(() => {
processor = new Processor({
namer,
});
});

it("should include exported classes from the composed file", async () => {
const { exports } = await processor.string(id, dedent(`
@composes "./local.css";
.a {
color: aqua;
}
.b {
color: blue;
}
`));

expect(exports).toMatchSnapshot();
});

it("should not include exported values from the composed file", async () => {
const { exports } = await processor.string(id, dedent(`
@composes "./values.css";
`));

expect(exports).toMatchSnapshot();
});

it("should allow composing classes from the composed file", async () => {
const { exports } = await processor.string(id, dedent(`
@composes "./simple.css";
.a {
composes: wooga;
color: aqua;
}
.b {
color: blue;
}
`));

expect(exports).toMatchSnapshot();
});

it("should include compositions from the composed file", async () => {
await processor.string(id, dedent(`
@composes "./simple.css";
.a {
color: aqua;
}
.b {
composes: wooga;
color: blue;
}
`));

const { compositions } = await processor.output();

expect(compositions).toMatchSnapshot();
});

it("should output css from the composed file", async () => {
await processor.string(id, dedent(`
@composes "./simple.css";
.a {
color: aqua;
}
.b {
composes: wooga;
color: blue;
}
`));

const { css } = await processor.output();

expect(css).toMatchSnapshot();
});

it("should allow for chains of @composes-included files", async () => {
const { exports } = await processor.string("./packages/processor/test/specimens/at-composes-child.css", dedent(`
@composes "./at-composes.css";
.a {
composes: notblue;
color: aqua;
}
.b {
composes: wooga;
color: blue;
}
`));

expect(exports).toMatchSnapshot();
});

it("should only allow a single @composes per file", async () => {
try {
await processor.string(id, dedent(`
@composes "./simple.css";
@composes "./blue.css";
`));
} catch({ message }) {
expect(message).toMatch(`Only one @composes rule per file`);
}
});
});
});
@@ -45,7 +45,6 @@ describe("/processor.js", () => {
.e:not(.e) {}
`)
);


expect(exports).toMatchSnapshot();

@@ -0,0 +1,7 @@
@composes "./blue.css";

.notblue {
composes: wooga;

color: green;
}

0 comments on commit 27d696a

Please sign in to comment.
You can’t perform that action at this time.