|
| 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