Skip to content

Commit a43e6a7

Browse files
committed
Feat: add caveats
1 parent d320168 commit a43e6a7

File tree

2 files changed

+155
-0
lines changed

2 files changed

+155
-0
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This is a plugin of [rcs-core](https://github.com/JPeer264/node-rcs-core)
1616
- [Installation](#installation)
1717
- [Usage](#usage)
1818
- [Methods](#methods)
19+
- [Caveats](#caveats)
1920
- [LICENSE](#license)
2021

2122
## Installation
@@ -102,6 +103,11 @@ try {
102103
- [rcs.loadMapping](docs/api/loadMapping.md)
103104
- [rcs.config](docs/api/config.md)
104105

106+
## Caveats
107+
108+
Correctly using `rename-css-selectors` on large project means few rules should be followed.
109+
[This document](docs/caveats.md) explains most of them.
110+
105111
# LICENSE
106112

107113
MIT © [Jan Peer Stöcklmair](https://www.jpeer.at)

docs/api/caveats.md

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Using rename-css-selectors correctly and patterns to avoid
2+
3+
`rename-css-selectors` is using `rcs-core` to perform its CSS selectors optimization.
4+
The core is performing three steps on your files:
5+
1. Extracting CSS's class and id selectors, keyframes, variables and attributes selectors
6+
2. Assigning minified value for each entry it has found
7+
3. Trying to replace all elements that could match in your files with the minified version
8+
9+
## Caveats for step 1
10+
11+
> **tl;dr:** Selectors are just renamed if they appear in your CSS
12+
13+
Currently, the first step is done on CSS syntax only (either from CSS files or extracted from HTML's `<style>` tags).
14+
This means that if you have some HTML code doing this:
15+
16+
```html
17+
<div class='something'>[...]</div>
18+
<div class='something'>[...]</div>
19+
```
20+
21+
without a CSS rule for class's `something`, it'll not be replaced/minified.
22+
In the future, the core might add a notice for these cases.
23+
24+
HTML replacement should be quite solid for now, since the core can inspect attribute's name and only act upon `class` and `id` (and label's `for`).
25+
26+
## Caveats for step 2
27+
28+
> **tl;dr:** Do not name your selectors like standard HTML attributes. Such selectors are [excluded by default](https://github.com/JPeer264/node-rcs-core/blob/master/lib/helpers/excludeList.ts)
29+
30+
It's very important that the assigned *minified* name for each entry does not collide with a valid CSS selector that you could encounter.
31+
Paradoxically, if your initial selectors are very large, you'll have less chances to encounter an issue. Using `rename-css-selectors` in your pipeline should encourage more meaningful selectors in your file.
32+
33+
By default, the core tries to minimize the length of the mapped minified name's selector. So if you have a CSS selector in your code like `.something`, it'll be mapped to (for example), `.a` if available.
34+
If you already have a small selector (like `.b`), the core is smart enough to map it to a similar length's selector (but not necessarly `.b`, it can be `.t`). When all first letter selectors are used, it's generating two letter selectors and so on.
35+
36+
As you'll see in the next section, because of JavaScript's openness, some selectors can be understood differently.
37+
38+
Imagine you have such document:
39+
```html
40+
<style>
41+
.b { color: red; }
42+
b { color: blue; }
43+
</style>
44+
45+
<div class='b'>
46+
<b>Yeah</b>
47+
</div>
48+
49+
<script>
50+
const a = 'b';
51+
const c = document.querySelector(a);
52+
const d = document.getElementsByClassName(a);
53+
</script>
54+
```
55+
Here, the *key* `'b'` in the JavaScript code can refer to the tag name `<b>` (when being called by `document.querySelector`) or to the class `.b` (when being called by `document.getElementsByClassName`.
56+
The core does not run your code (it only parses it) and, as such, can't figure out when a variable is being used for the former or latter case.
57+
Please notice that the core can't expect a *class* selection by looking at `getElementsByClassName` since you can be using any library that's abstracting this (like jQuery, React...).
58+
59+
So, to avoid this caveat, you can either:
60+
* Avoid conflicting class name with potential HTML tag name (don't name your CSS class like `.b` or `.i` or `.span`, ...)
61+
* Avoid using `getElementById`, `getElementsByClassName` in your JavaScript code (and only fallback to `querySelector` and `querySelectorsAll`)
62+
* Reserve some mapping so the core can't use them (check the `config.reserve` array in the documentation about how to do that)
63+
* Exclude any selector that's conflicting with any HTML tag name (check the `config.exclude` array in the documentation about how to do that)
64+
65+
66+
### Warning:
67+
68+
In the example above, `<div class='b'>` can be what the core is generating for your initial class `something`. So the example above could have been generated from this source:
69+
70+
```html
71+
<style>
72+
.something { color: red; }
73+
b { color: blue; }
74+
</style>
75+
<div class='something'>
76+
<b>Yeah</b>
77+
</div>
78+
<script>
79+
const a = 'something';
80+
const c = document.querySelector(a);
81+
const d = document.getElementsByClassName(a);
82+
</script>
83+
```
84+
In that case, the initial code is semantically wrong since the `querySelector` should return `undefined`. The minified CSS selector code will thus return a different result in that case.
85+
86+
If you had followed the advice above and excluded (or reserved) all potential HTML tag name (see below), then `something` won't be mapped to `b`, but, for example, `t` and the initial code behavior will be preserved.
87+
88+
Similarly, if your initial code was:
89+
```js
90+
...
91+
const a = 'b';
92+
...
93+
```
94+
then the core will generate a warning telling you that `b` is already used in the renaming map and that you should fix the ambiguity.
95+
96+
By default all HTML and CSS attributes are excluded, see [here](https://github.com/JPeer264/node-rcs-core/blob/master/lib/helpers/excludeList.ts)
97+
98+
The incovenient being that such selectors won't be minified (think about `header` for example, it's quite common for a class name)
99+
100+
## Caveat for step 3
101+
102+
> **tl;dr:** Be careful with CSS selectors in JavaScript. Do not use string concatenation with your CSS selectors.
103+
104+
Replacement is relatively safe for CSS and HTML (again, because such format are explicit enough to avoiding ambiguity).
105+
106+
Replacements inside JavaScript code is much harder because a string for a selector can be generated from many small parts (like in `const a = '.' + objClass;`), can be used in functions that have different semantics (like `getElementById` and `querySelector`), etc...
107+
108+
So, here's the way the core is trying to resolve ambiguities (but as seen above, not all ambiguities can be solved):
109+
110+
1. ECMAScript template, like in ``const a = `some text ${jsConst}`;`` is parsed like HTML, looking for `class="X"` or `id='Y'` or `for="Z"`.
111+
If you use template code to make your CSS selector from each part, it'll fail:
112+
Don't write ``const myClass = 'something'; const sel = `.${myClass}`;``, but instead `const myClass = '.something'`.
113+
114+
2. Each string literal is extracted from your JavaScript code and:
115+
- If it contains some CSS specific selectors chars (like `#` or `.` or ` ` etc...), then it's parsed as a CSS selector. **This is safer and usually gives the expected result.**
116+
- If it does not contain some CSS specific selectors (like a single word), then it's tried as an ID selector first and if not found as a class selector (or not replaced if not found in either one).
117+
**This can lead to the ambiguity shown above.**
118+
119+
So, to avoid such ambiguity, try to store your class or id with their respective prefix (`.` or `#`) in your code, and rely on `querySelector` et al.
120+
121+
For example, here are some replacement selected by the library, with these CSS selector:
122+
123+
```css
124+
.something {} /* Mapped to .a */
125+
#test {} /* Mapped to #a */
126+
.test {} /* Mapped to .b */
127+
```
128+
| Initial code | Replaced code | Reason |
129+
|---|---|---|
130+
|`const a = 'something';`|`const a = 'b';`| The core could not deduce the string literal is a selector, so fallback to searching for any rule with `something`, class had one|
131+
|`const a = 'test';`|`const a = 'a';`| The core could not deduce the string literal is a selector and in fallback to search for any rule, it selected #test=>#a since it was searched first|
132+
|`const a = '.something';`|`const a = '.a';`| The core deduced a class selector was required and replaced it|
133+
|`const a = '.test';`|`const a = '.b';`| The core deduced a class selector was required and replaced it|
134+
|`const a = ' something';`|`const a = ' something';`| The core deduced the string literal is a CSS selector, but could not find any match|
135+
|`const a = 'div[class=something]';`|`const a = 'div[class=a]';`| The core deduced the attribute selector was related to a class selector and replaced it|
136+
|`const a = 'input[name=something]';`|`const a = 'input[name=something]';`| The core deduced the attribute selector wasn't related to a class, id or for attribute, no replacement was done|
137+
|`const a = 'b #test, .something';`|`const a = 'b #a, .a';`| The core parsed all selectors and produced correct replacement for each of them|
138+
139+
### Another remark:
140+
Some are writing CSS selector this way:
141+
```css
142+
div[class = something]
143+
{
144+
color: red;
145+
}
146+
```
147+
This is perfectly safe from JavaScript code (if you have `const a = 'div[class=something]';`), then this will be replaced correctly by the core (*to* `const a = 'div[class=b]';`).
148+
149+
This however requires much more work from the developper so this is not a solution we recommend.

0 commit comments

Comments
 (0)