Skip to content

Commit d62b204

Browse files
ramithachriseppstein
authored andcommitted
feat: Creating a new package for bem to css-blocks conversion.
- This creates a new npm package for converting BEM files to block files - It also contains changes to the CLI that adds a convert method - It iterates through the .css file, converting each of the classNames to comply with CSS blocks and rewrite the same file. - The same should work for .scss files TODO - Solve for when there is more than one block in a single file - Add more tests around the plugin
1 parent 38601cb commit d62b204

22 files changed

+723
-2419
lines changed

css-blocks.code-workspace

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
},
4949
{
5050
"path": "packages/@css-blocks/config"
51+
},
52+
{
53+
"path": "packages/@css-blocks/bem-to-blocks"
5154
}
5255
],
5356
"settings": {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Change Log
2+
3+
All notable changes to this project will be documented in this file.
4+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5+
6+
# [1.0.0-alpha.1](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2019-12-10)
7+
8+
**Note:** Version bump only for package @css-blocks/code-style
9+
10+
11+
12+
13+
14+
# [0.24.0](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.23.2...v0.24.0) (2019-09-16)
15+
16+
**Note:** Version bump only for package @css-blocks/code-style
17+
18+
19+
20+
21+
22+
<a name="0.23.0"></a>
23+
# [0.23.0](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.22.0...v0.23.0) (2019-05-08)
24+
25+
**Note:** Version bump only for package @css-blocks/code-style
26+
27+
28+
29+
30+
31+
<a name="0.22.0"></a>
32+
# [0.22.0](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.21.0...v0.22.0) (2019-05-02)
33+
34+
**Note:** Version bump only for package @css-blocks/code-style
35+
36+
37+
38+
39+
40+
<a name="0.21.0"></a>
41+
# [0.21.0](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.20.0...v0.21.0) (2019-04-07)
42+
43+
**Note:** Version bump only for package @css-blocks/code-style
44+
45+
46+
47+
48+
49+
<a name="0.20.0"></a>
50+
# [0.20.0](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.20.0-beta.8...v0.20.0) (2019-03-11)
51+
52+
**Note:** Version bump only for package @css-blocks/code-style
53+
54+
55+
56+
57+
58+
<a name="0.20.0-beta.7"></a>
59+
# [0.20.0-beta.7](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.20.0-beta.5...v0.20.0-beta.7) (2019-02-01)
60+
61+
**Note:** Version bump only for package @css-blocks/code-style
62+
63+
64+
65+
66+
67+
<a name="0.20.0-beta.6"></a>
68+
# [0.20.0-beta.6](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.20.0-beta.5...v0.20.0-beta.6) (2019-02-01)
69+
70+
**Note:** Version bump only for package @css-blocks/code-style
71+
72+
73+
74+
75+
76+
<a name="0.20.0-beta.5"></a>
77+
# [0.20.0-beta.5](https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/code-style/compare/v0.20.0-beta.4...v0.20.0-beta.5) (2019-01-08)
78+
79+
**Note:** Version bump only for package @css-blocks/code-style
80+
81+
82+
83+
84+
85+
<a name="0.20.0-beta.4"></a>
86+
# [0.20.0-beta.4](https://github.com/linkedin/css-blocks/compare/v0.20.0-beta.3...v0.20.0-beta.4) (2018-10-19)
87+
88+
89+
### Features
90+
91+
* Manually throw error for Node 6 in Analyzer. ([5788fcc](https://github.com/linkedin/css-blocks/commit/5788fcc))
92+
93+
94+
95+
96+
97+
<a name="0.18.0"></a>
98+
# [0.18.0](https://github.com/linkedin/css-blocks/compare/0.15.1...0.18.0) (2018-04-24)
99+
100+
101+
### Features
102+
103+
* Enable root-level typedoc generation for the project. ([59c85a3](https://github.com/linkedin/css-blocks/commit/59c85a3))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# BEM to CSS Blocks
2+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "http://json.schemastore.org/tslint",
3+
"extends": "@opticss/code-style/configs/tslint.cli.json"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "http://json.schemastore.org/tslint",
3+
"extends": "@opticss/code-style/configs/tslint.interactive.json"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "http://json.schemastore.org/tslint",
3+
"extends": "@opticss/code-style/configs/tslint.release.json"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"name": "@css-blocks/bem-to-blocks",
3+
"author": "Ramitha Chitloor",
4+
"description": "Tools to convert BEM files to CSS block files.",
5+
"license": "BSD-2-Clause",
6+
"version": "1.0.0-alpha.4",
7+
"main": "dist/src/index.js",
8+
"types": "dist/src/index.d.ts",
9+
"files": [
10+
"bin",
11+
"dist",
12+
"src",
13+
"README.md",
14+
"CHANGELOG.md"
15+
],
16+
"readme": "README.md",
17+
"keywords": [
18+
"tslint",
19+
"tslint-plugin"
20+
],
21+
"scripts": {
22+
"test": "yarn run test:runner",
23+
"test:runner": "mocha --opts test/mocha.opts dist/test",
24+
"compile": "tsc --build",
25+
"pretest": "yarn run compile",
26+
"posttest": "yarn run lint",
27+
"prepublish": "rm -rf dist && yarn run compile && yarn run lintall",
28+
"lint": "tslint -t msbuild --project . -c tslint.cli.json",
29+
"lintall": "tslint -t msbuild --project . -c tslint.release.json",
30+
"lintfix": "tslint -t msbuild --project . -c tslint.cli.json --fix",
31+
"coverage": "istanbul cover -i dist/src/**/*.js --dir ./build/coverage node_modules/mocha/bin/_mocha -- dist/test --opts test/mocha.opts",
32+
"remap": "remap-istanbul -i build/coverage/coverage.json -o coverage -t html",
33+
"watch": "watch 'yarn run test' src test --wait=1"
34+
},
35+
"bugs": {
36+
"url": "https://github.com/linkedin/css-blocks/issues"
37+
},
38+
"repository": "https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/bem-to-blocks",
39+
"homepage": "https://github.com/linkedin/css-blocks/tree/master/packages/%40css-blocks/bem-to-blocks#readme",
40+
"publishConfig": {
41+
"access": "public"
42+
},
43+
"dependencies": {
44+
"@css-blocks/code-style": "^1.0.0-alpha.1",
45+
"opticss": "^0.7.0",
46+
"postcss": "^7.0.14"
47+
},
48+
"engines": {
49+
"node": "6.* || 8.* || >= 10.*"
50+
},
51+
"volta": {
52+
"node": "12.2.0",
53+
"yarn": "1.21.0"
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import * as fs from "fs-extra";
2+
import { ParsedSelector, SelectorCache } from "opticss";
3+
import * as postcss from "postcss";
4+
5+
import { BemSelector, BlockClassSelector } from "./interface";
6+
import { findLcs } from "./utils";
7+
export declare type PostcssAny = unknown;
8+
9+
type BemSelectorMap = Map<string, BemSelector>;
10+
type ElementToBemSelectorMap = Map<string, BemSelector[]>;
11+
type BlockToBemSelectorMap = Map<string, ElementToBemSelectorMap>;
12+
type BemToBlockClassMap = WeakMap<BemSelector, BlockClassSelector>;
13+
14+
const EMPTY_ELEMENT_PLACEHOLDER = "EMPTY-ELEMENT-PLACEHOLDER";
15+
16+
export function convertBemToBlocks(files: Array<string>): Promise<void>[] {
17+
let promises: Promise<void>[] = [];
18+
files.forEach(file => {
19+
fs.readFile(file, (_err, css) => {
20+
let output = postcss([bemToBlocksPlugin])
21+
.process(css, { from: file });
22+
// rewrite the file with the processed output
23+
promises.push(fs.writeFile(file, output.toString()));
24+
});
25+
});
26+
return promises;
27+
}
28+
29+
/**
30+
* Iterates through a cache of bemSelectors and returns a map of bemSelector to
31+
* the blockClassName. This function optimises the states and subStates from the
32+
* name of the modifier present in the BEM selector
33+
* @param bemSelectorCache weakmap - BemSelectorMap
34+
BemSelector {
35+
block: 'jobs-hero',
36+
element: 'image-container',
37+
modifier: undefined }
38+
=>
39+
BlockClassName {
40+
class: // name of the element if present. If this is not present, then it is on the :scope
41+
state: // name of the modifiers HCF
42+
subState: // null if HCF is null
43+
}, // written to a file with blockname.block.css
44+
*/
45+
export function constructBlocksMap(bemSelectorCache: BemSelectorMap): BemToBlockClassMap {
46+
let blockListMap: BlockToBemSelectorMap = new Map();
47+
let resultMap: BemToBlockClassMap = new WeakMap();
48+
49+
// create the resultMap and the blockListMap
50+
for (let bemSelector of bemSelectorCache.values()) {
51+
// create the new blockClass instance
52+
let blockClass: BlockClassSelector = new BlockClassSelector();
53+
if (bemSelector.element) {
54+
blockClass.class = bemSelector.element;
55+
}
56+
if (bemSelector.modifier) {
57+
blockClass.state = bemSelector.modifier;
58+
}
59+
// add this blockClass to the resultMap
60+
resultMap.set(bemSelector, blockClass);
61+
62+
// add this selector to the blockList based on the block, and then the
63+
// element value
64+
let block = blockListMap.get(bemSelector.block);
65+
if (block) {
66+
if (bemSelector.element) {
67+
if (block.has(bemSelector.element)) {
68+
(block.get(bemSelector.element) as BemSelector[]).push(bemSelector);
69+
} else {
70+
block.set(bemSelector.element, new Array(bemSelector));
71+
}
72+
} else {
73+
// the modifier is on the block itself
74+
if (block.has(EMPTY_ELEMENT_PLACEHOLDER)) {
75+
(block.get(EMPTY_ELEMENT_PLACEHOLDER) as BemSelector[]).push(bemSelector);
76+
} else {
77+
block.set(EMPTY_ELEMENT_PLACEHOLDER, new Array(bemSelector));
78+
}
79+
}
80+
} else {
81+
// if there is no existing block, create the elementMap and the add it to
82+
// the blockMap
83+
let elementListMap = new Map();
84+
if (bemSelector.element) {
85+
elementListMap.set(bemSelector.element, new Array(bemSelector));
86+
} else {
87+
elementListMap.set(EMPTY_ELEMENT_PLACEHOLDER, new Array(bemSelector));
88+
}
89+
blockListMap.set(bemSelector.block, elementListMap);
90+
}
91+
}
92+
93+
// optimize the blocks for sub-states, iterate through the blocks
94+
for (let elementListMap of blockListMap.values()) {
95+
// iterate through the elements
96+
for (let selList of elementListMap.values()) {
97+
let lcs: string;
98+
// find the longest common substring(LCS) in the list of selectors
99+
let modifiers = selList.length && selList.filter(sel => sel.modifier !== undefined);
100+
if (modifiers) {
101+
if (modifiers.length > 1) {
102+
lcs = findLcs(modifiers.map(sel => sel.modifier as string));
103+
}
104+
// update the states and substates with the LCS
105+
modifiers.forEach(sel => {
106+
let blockClass = resultMap.get(sel);
107+
if (blockClass && lcs) {
108+
blockClass.subState = (blockClass.state as string).replace(lcs, "");
109+
blockClass.state = lcs.replace(/-$/, "");
110+
}
111+
});
112+
}
113+
}
114+
}
115+
// TODO: detect if there is a scope node, if not create a new empty scope node
116+
return resultMap;
117+
}
118+
119+
/**
120+
* PostCSS plugin for transforming BEM to CSS blocks
121+
*/
122+
export const bemToBlocksPlugin: postcss.Plugin<PostcssAny> = postcss.plugin("bem-to-blocks-plugin", (options) => {
123+
options = options || {};
124+
125+
return (root, result) => {
126+
const cache = new SelectorCache();
127+
const bemSelectorCache: BemSelectorMap = new Map();
128+
129+
// in this pass, we collect all the selectors
130+
root.walkRules(rule => {
131+
let parsedSelList = cache.getParsedSelectors(rule);
132+
parsedSelList.forEach(parsedSel => {
133+
parsedSel.eachSelectorNode(node => {
134+
if (node.value) {
135+
let bemSelector = new BemSelector(node.value);
136+
if (bemSelector) {
137+
// add it to the cache so it's available for the next pass
138+
bemSelectorCache.set(node.value, bemSelector);
139+
} else {
140+
console.error(`${parsedSel} does not comply with BEM standards. Consider a refactor`);
141+
}
142+
}
143+
});
144+
});
145+
});
146+
147+
// convert selectors to block selectors
148+
let bemToBlockClassMap: BemToBlockClassMap = constructBlocksMap(bemSelectorCache);
149+
150+
// rewrite into a CSS block
151+
root.walkRules(rule => {
152+
// iterate through each rule
153+
let parsedSelList = cache.getParsedSelectors(rule);
154+
let modifiedSelList: ParsedSelector[] = new Array();
155+
parsedSelList.forEach(sel => {
156+
// this contains the selector combinators
157+
let modifiedCompoundSelector = sel.clone();
158+
159+
modifiedCompoundSelector.eachSelectorNode(node => {
160+
// we only need to modify class names. We can ignore everything else,
161+
// like existing attributes, pseudo selectors, comments, imports,
162+
// exports, etc
163+
if (node.type === "class" && node.value) {
164+
let bemSelector = bemSelectorCache.get(node.value);
165+
// get the block class from the bemSelector
166+
let blockClassName = bemSelector && bemToBlockClassMap.get(bemSelector);
167+
168+
// if the selector was previously cached
169+
if (blockClassName) {
170+
// we need to use the below method instead of node.value as the
171+
// attributes brackets get escaped when doing node.value = blockClassName.toString()
172+
node.setPropertyWithoutEscape("value", blockClassName.toString());
173+
}
174+
}
175+
});
176+
177+
modifiedSelList.push(modifiedCompoundSelector);
178+
});
179+
180+
// if the selector nodes were modified, then create a new rule for it
181+
if (modifiedSelList.toString()) {
182+
let newRule = rule.clone();
183+
newRule.selector = modifiedSelList.toString();
184+
rule.replaceWith(newRule);
185+
}
186+
});
187+
result.root = root;
188+
};
189+
});

0 commit comments

Comments
 (0)