|
| 1 | +# Use existing plugins |
| 2 | + |
| 3 | +We can save time if there's already a [PostCSS plugin](https://postcss.org/docs/postcss-plugins) that meets your needs. |
| 4 | + |
| 5 | +As a concrete example, we'll use [`postcss-nested`](https://github.com/postcss/postcss-nested) to _remove_ nested code. We may want to do this for a few different reasons: |
| 6 | + |
| 7 | +- We want to migrate away from Sass. |
| 8 | +- We prefer native CSS while [CSS nesting remains in spec](https://www.w3.org/TR/css-nesting-1/). |
| 9 | +- We want to remove a PostCSS plugin (our project has too many plugins). |
| 10 | +- We use [CSS modules](https://github.com/css-modules/css-modules) (class selectors are hashed) so nesting isn't needed. |
| 11 | + |
| 12 | + |
| 13 | +## Use the CLI |
| 14 | + |
| 15 | +Change the directory to a place where you like to keep projects. Then, run these commands: |
| 16 | + |
| 17 | +```sh |
| 18 | +# Create project |
| 19 | +npx @codemod-utils/cli write-native-css |
| 20 | + |
| 21 | +# Install dependencies |
| 22 | +cd write-native-css |
| 23 | +pnpm install |
| 24 | + |
| 25 | +# Install postcss and postcss-nested as dependencies |
| 26 | +pnpm install postcss and postcss-nested |
| 27 | +``` |
| 28 | + |
| 29 | +> [!NOTE] |
| 30 | +> Just like in [the main tutorial](../main-tutorial/04-step-1-update-acceptance-tests-part-1.md#remove-the-sample-step), remove the sample step, `add-end-of-line`. |
| 31 | +
|
| 32 | + |
| 33 | +## Scaffold step |
| 34 | + |
| 35 | +Create a step called `remove-css-nesting`. It is to read `*.css` files and write back the file content (a no-op). |
| 36 | + |
| 37 | +<details> |
| 38 | + |
| 39 | +<summary><code>src/steps/remove-css-nesting.ts</code></summary> |
| 40 | + |
| 41 | +For brevity, how `src/index.ts` calls `removeCssNesting()` is not shown. |
| 42 | + |
| 43 | +```ts |
| 44 | +import { readFileSync } from 'node:fs'; |
| 45 | +import { join } from 'node:path'; |
| 46 | + |
| 47 | +import { createFiles, findFiles } from '@codemod-utils/files'; |
| 48 | + |
| 49 | +import { Options } from '../types/index.js'; |
| 50 | + |
| 51 | +export function removeCssNesting(options: Options): void { |
| 52 | + const { projectRoot } = options; |
| 53 | + |
| 54 | + const filePaths = findFiles('app/**/*.css', { |
| 55 | + projectRoot, |
| 56 | + }); |
| 57 | + |
| 58 | + const fileMap = new Map( |
| 59 | + filePaths.map((filePath) => { |
| 60 | + const oldFile = readFileSync(join(projectRoot, filePath), 'utf8'); |
| 61 | + |
| 62 | + return [filePath, oldFile]; |
| 63 | + }), |
| 64 | + ); |
| 65 | + |
| 66 | + createFiles(fileMap, options); |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +</details> |
| 71 | + |
| 72 | +To test the step, here's a stylesheet with nested code: |
| 73 | + |
| 74 | +<details> |
| 75 | + |
| 76 | +<summary><code>tests/fixtures/sample-project/input/app/components/ui/page.css</code></summary> |
| 77 | + |
| 78 | +Note, the syntax `@value` is specific to CSS modules. We will later replace it with `var()` from native CSS. |
| 79 | + |
| 80 | +```css |
| 81 | +@value ( |
| 82 | + desktop, |
| 83 | + spacing-400, |
| 84 | + spacing-600 |
| 85 | +) from "my-design-tokens"; |
| 86 | + |
| 87 | +@value navigation-menu-height: 3rem; |
| 88 | + |
| 89 | +.container { |
| 90 | + display: grid; |
| 91 | + grid-template-areas: |
| 92 | + "header" |
| 93 | + "body"; |
| 94 | + grid-template-columns: 1fr; |
| 95 | + grid-template-rows: auto 1fr; |
| 96 | + height: calc(100% - navigation-menu-height); |
| 97 | + overflow-y: auto; |
| 98 | + padding: spacing-600 spacing-400; |
| 99 | + scrollbar-gutter: stable; |
| 100 | + |
| 101 | + .header { |
| 102 | + grid-area: header; |
| 103 | + } |
| 104 | + |
| 105 | + .body { |
| 106 | + grid-area: body; |
| 107 | + } |
| 108 | + |
| 109 | + @media desktop { |
| 110 | + grid-template-areas: |
| 111 | + "header body"; |
| 112 | + grid-template-columns: auto 1fr; |
| 113 | + grid-template-rows: 1fr; |
| 114 | + height: 100%; |
| 115 | + } |
| 116 | +} |
| 117 | +``` |
| 118 | + |
| 119 | +</details> |
| 120 | + |
| 121 | + |
| 122 | +## Update step |
| 123 | + |
| 124 | +Next, we use the `postcss-nested` plugin to update the file. |
| 125 | + |
| 126 | +```diff |
| 127 | +import { readFileSync } from 'node:fs'; |
| 128 | +import { join } from 'node:path'; |
| 129 | + |
| 130 | +import { createFiles, findFiles } from '@codemod-utils/files'; |
| 131 | ++ import postcss from 'postcss'; |
| 132 | ++ import PostcssNestedPlugin from 'postcss-nested'; |
| 133 | + |
| 134 | +import { Options } from '../types/index.js'; |
| 135 | + |
| 136 | ++ function updateFile(file: string): string { |
| 137 | ++ const plugins = [PostcssNestedPlugin()]; |
| 138 | ++ |
| 139 | ++ return postcss(plugins).process(file).css; |
| 140 | ++ } |
| 141 | ++ |
| 142 | +export function removeCssNesting(options: Options): void { |
| 143 | + const { projectRoot } = options; |
| 144 | + |
| 145 | + const filePaths = findFiles('app/**/*.css', { |
| 146 | + projectRoot, |
| 147 | + }); |
| 148 | + |
| 149 | + const fileMap = new Map( |
| 150 | + filePaths.map((filePath) => { |
| 151 | + const oldFile = readFileSync(join(projectRoot, filePath), 'utf8'); |
| 152 | ++ const newFile = updateFile(oldFile); |
| 153 | + |
| 154 | +- return [filePath, oldFile]; |
| 155 | ++ return [filePath, newFile]; |
| 156 | + }), |
| 157 | + ); |
| 158 | + |
| 159 | + createFiles(fileMap, options); |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +Run `./update-test-fixtures.sh`. You will see that `.header`, `.body`, and `@media` blocks are no longer inside the `.container` block. |
| 164 | + |
| 165 | +<details> |
| 166 | + |
| 167 | +<summary><code>tests/fixtures/sample-project/output/app/components/ui/page.css</code></summary> |
| 168 | + |
| 169 | +```css |
| 170 | +@value ( |
| 171 | + desktop, |
| 172 | + spacing-400, |
| 173 | + spacing-600 |
| 174 | +) from "my-design-tokens"; |
| 175 | + |
| 176 | +@value navigation-menu-height: 3rem; |
| 177 | + |
| 178 | +.container { |
| 179 | + display: grid; |
| 180 | + grid-template-areas: |
| 181 | + "header" |
| 182 | + "body"; |
| 183 | + grid-template-columns: 1fr; |
| 184 | + grid-template-rows: auto 1fr; |
| 185 | + height: calc(100% - navigation-menu-height); |
| 186 | + overflow-y: auto; |
| 187 | + padding: spacing-600 spacing-400; |
| 188 | + scrollbar-gutter: stable; |
| 189 | +} |
| 190 | + |
| 191 | +.container .header { |
| 192 | + grid-area: header; |
| 193 | + } |
| 194 | + |
| 195 | +.container .body { |
| 196 | + grid-area: body; |
| 197 | + } |
| 198 | + |
| 199 | +@media desktop { |
| 200 | + |
| 201 | +.container { |
| 202 | + grid-template-areas: |
| 203 | + "header body"; |
| 204 | + grid-template-columns: auto 1fr; |
| 205 | + grid-template-rows: 1fr; |
| 206 | + height: 100% |
| 207 | +} |
| 208 | + } |
| 209 | +``` |
| 210 | + |
| 211 | +</details> |
| 212 | + |
| 213 | +> [!TIP] |
| 214 | +> Often, formatting can't be preserved. Ask the consuming project to use `prettier` and `stylelint` so that you can separate formatting concerns. |
| 215 | + |
| 216 | + |
| 217 | +<div align="center"> |
| 218 | + <div> |
| 219 | + Next: <a href="./02-write-custom-plugins.md">Write custom plugins</a> |
| 220 | + </div> |
| 221 | + <div> |
| 222 | + Previous: <a href="./00-introduction.md">Introduction</a> |
| 223 | + </div> |
| 224 | +</div> |
0 commit comments