Skip to content

Commit

Permalink
add mode option
Browse files Browse the repository at this point in the history
  • Loading branch information
billouboq committed Aug 11, 2018
1 parent 5bd325c commit af698a9
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 71 deletions.
9 changes: 9 additions & 0 deletions __tests__/assets/style-1.all.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
a {
font-size: 12px;
color: blue;
font-family: Roboto;
}

a {
font-size: 13px;
}
File renamed without changes.
5 changes: 5 additions & 0 deletions __tests__/assets/style-2.all.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
a {
font-weight: bold;
color: red;
font-family: Roboto;
}
File renamed without changes.
68 changes: 62 additions & 6 deletions __tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const postcss = require("postcss");
const slashCSS = require("../index");
const { MODES } = require("../src/constants");

function run(input, output, opts) {
return postcss([slashCSS(opts)])
Expand All @@ -20,17 +21,37 @@ describe("Test main functions", () => {
});

it("Should throw an error if targets option is not a string", async () => {
expect(() => slashCSS({ targets: 10 })).toThrow(
expect(() => slashCSS({
targets: 10
})).toThrow(
"Targets option must be a string"
);
});

it("Should throw an error if mode option is not a string", async () => {
expect(() => slashCSS({
targets: "./__tests__/assets/**/*.css",
mode: 12
})).toThrow(
"Mode option must be a string"
);
});

it("Should throw an error if mode option is not a valid mode", async () => {
expect(() => slashCSS({
targets: "./__tests__/assets/**/*.css",
mode: "tezrzr"
})).toThrow(
"Invalid mode option value"
);
});

it("Should throw an error if no css files are found", async () => {
expect(
run(
"a{font-size: 12px; color: blue; font-family: Roboto; position: relative;}",
"a{position:relative;}",
{ targets: "./rezaraze.css" }
{targets: "./rezaraze.css"}
)
).rejects.toThrow("No css files found");
});
Expand All @@ -39,13 +60,48 @@ describe("Test main functions", () => {
return run(
"a{font-size: 12px; color: blue; font-family: Roboto; position: relative;}",
"a{position:relative;}",
{ targets: "./__tests__/assets/**/*.css" }
{targets: "./__tests__/assets/**/*.atleast.css"}
);
});

it("Should remove duplicate css selector since all props are removed", () => {
return run("a{font-size: 12px; color: blue; font-family: Roboto;}", "", {
targets: "./__tests__/assets/**/*.css",
});
return run(
"a{font-size: 12px; color: blue; font-family: Roboto;}",
"",
{targets: "./__tests__/assets/**/*.atleast.css"}
);
});

it("Should remove duplicate css properties MatchAtLeastOne mode", () => {
return run(
"a{font-size: 12px; color: blue; font-family: Roboto;}",
"",
{
targets: "./__tests__/assets/**/*.atleast.css",
mode: MODES.ATLEAST_ONE
}
);
});

it("Should remove duplicate css properties MatchAll mode", () => {
return run(
"a{font-size: 12px; color: blue; font-family: Roboto;}",
"a{font-size:12px;color:blue;}",
{
targets: "./__tests__/assets/**/*.all.css",
mode: MODES.ALL
}
);
});

it("Should not remove anything since it does not match all targets files", () => {
return run(
"a{font-size: 12px;}",
"a{font-size:12px;}",
{
targets: "./__tests__/assets/**/*.all.css",
mode: MODES.ALL
}
);
});
});
67 changes: 2 additions & 65 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,69 +1,6 @@
"use strict";

const fs = require("fs");
const util = require("util");
const postcss = require("postcss");
const glob = require("fast-glob");
const slashCSS = require("./src/slashCSS");

const getFileContent = util.promisify(fs.readFile);

function slashCSSPlugin(opts = {}) {
if (!opts || !opts.targets) {
throw new Error(
"This plugins needs an option object with a targets propertie"
);
}

if (typeof opts.targets !== "string") {
throw new Error("Targets option must be a string");
}

return async root => {
try {
// get all external targets files
const cssFilesPath = await glob(opts.targets);

if (!cssFilesPath.length) {
throw new Error("No css files found");
}

const getFileContentPromises = cssFilesPath.map(filePath =>
getFileContent(filePath, "utf-8")
);
const cssFilesContent = await Promise.all(getFileContentPromises);

cssFilesContent.forEach(targetCSSContent => {
const targetsAST = postcss.parse(targetCSSContent).nodes;

root.walkRules(rule => {
// search for duplicate selector
const findedAst = targetsAST.find(
ast => ast.selector === rule.selector
);

if (findedAst) {
rule.walkDecls(function(decl) {
// if css properties are the sames (props and value) remove it
if (
findedAst.nodes.some(
prop => prop.prop === decl.prop && prop.value === decl.value
)
) {
decl.remove();
}
});

// if selector doesn't have any props then remove selector
if (!rule.nodes || !rule.nodes.length) {
rule.remove();
}
}
});
});
} catch (err) {
throw err;
}
};
}

module.exports = postcss.plugin("slashcss", slashCSSPlugin);
module.exports = postcss.plugin("slashcss", slashCSS);
80 changes: 80 additions & 0 deletions src/ast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use strict";

const { MODES } = require("./constants");

module.exports = {
formatAST,
getSingleFormatedAST,
}

function formatAST(ast) {
const formatedAST = {};

ast.forEach(item => {
if (!formatedAST[item.selector]) {
formatedAST[item.selector] = [];
}

item.nodes.forEach(({ prop, value }) => {
formatedAST[item.selector].push(`${prop}|${value}`);
});
});

return formatedAST;
}

function getSingleFormatedAST({asts, mode = MODES.ATLEAST_ONE} = {}) {
if (!asts || !asts.length) {
return {};
}

if (asts.length === 1) {
return formatedASTs[0];
}

const singleAST = {};

const formatForAtleastOne = () => {
asts.forEach(ast => {
Object.keys(ast).forEach(selector => {
if (!singleAST[selector]) {
singleAST[selector] = [];
}

ast[selector].forEach(prop => {
singleAST[selector].push(prop);
});
});
});
};

const formatForAll = () => {
const firstAST = asts.shift();

Object.keys(firstAST).forEach(selector => {
const allHaveSelector = asts.every(singleAst => singleAst[selector]);

if (allHaveSelector) {
const properties = firstAST[selector];

properties.forEach(propertie => {
if (asts.every(singleAst => singleAst[selector].includes(propertie))) {
if (!singleAST[selector]) {
singleAST[selector] = [];
}

singleAST[selector].push(propertie);
}
});
}
});
};

if (mode === MODES.ATLEAST_ONE) {
formatForAtleastOne();
} else {
formatForAll();
}

return singleAST;
}
8 changes: 8 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use strict";

module.exports = {
MODES: {
ATLEAST_ONE: "MatchAtleastOne",
ALL: "MatchAll"
}
}
8 changes: 8 additions & 0 deletions src/errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use strict";

module.exports = {
NO_CSS: "No css files found",
OPTIONS_MISSING: "This plugins needs an option object with a targets propertie",
OPTION_NOT_STRING: optionName => `${optionName} option must be a string`,
INVALID_OPTION: optionName => `Invalid ${optionName} option value`
}
75 changes: 75 additions & 0 deletions src/slashCSS.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"use strict";

const fs = require("fs");
const util = require("util");
const postcss = require("postcss");
const glob = require("fast-glob");

const ERRORS = require("./errors");
const { MODES } = require("./constants");
const { formatAST, getSingleFormatedAST } = require("./ast");

const getFileContent = util.promisify(fs.readFile);

module.exports = function slashCSSPlugin(opts = {}) {
const options = Object.assign({}, opts);

if (!options || !options.targets) {
throw new Error(ERRORS.OPTIONS_MISSING);
}

if (typeof options.targets !== "string") {
throw new Error(ERRORS.OPTION_NOT_STRING("Targets"));
}

if (options.mode) {
if (typeof options.mode !== "string") {
throw new Error(ERRORS.OPTION_NOT_STRING("Mode"));
}

if (Object.keys(MODES).every(key => MODES[key].toLowerCase() !== options.mode.toLowerCase())) {
throw new Error(ERRORS.INVALID_OPTION("mode"));
}
} else {
options.mode === MODES.ATLEAST_ONE;
}

return async (root) => {
try {
const cssFilesPath = await glob(options.targets);

if (!cssFilesPath.length) {
throw new Error(ERRORS.NO_CSS);
}

const getFileASTPromises = cssFilesPath.map(filePath => {
return getFileContent(filePath, "utf-8")
.then(cssContent => postcss.parse(cssContent).nodes)
.then(postCSSAST => formatAST(postCSSAST))
});

const singleAST = getSingleFormatedAST({
asts: await Promise.all(getFileASTPromises),
mode: options.mode
});

root.walkRules(rule => {
const targetSelector = singleAST[rule.selector];

if (targetSelector) {
rule.walkDecls(function (decl) {
if (targetSelector.includes(`${decl.prop}|${decl.value}`)) {
decl.remove();
}
});

if (!rule.nodes || !rule.nodes.length) {
rule.remove();
}
}
});
} catch (err) {
throw err;
}
}
};

0 comments on commit af698a9

Please sign in to comment.