Skip to content

Commit 695c8fa

Browse files
authored
Added tutorial for updating CSS files (#140)
Added postcss tutorial Co-authored-by: ijlee2 <ijlee2@users.noreply.github.com>
1 parent b2d617e commit 695c8fa

File tree

5 files changed

+715
-0
lines changed

5 files changed

+715
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ npx @codemod-utils/cli <your-codemod-name>
3030

3131
- [Main tutorial](./tutorials/main-tutorial/00-introduction.md)
3232
- [Create blueprints](./tutorials/create-blueprints/00-introduction.md)
33+
- [Update CSS files](./tutorials/update-css-files/00-introduction.md)
3334
- [Update `<template>` tags](./tutorials/update-template-tags/00-introduction.md)
3435

3536

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Introduction
2+
3+
> [!IMPORTANT]
4+
> Please complete the [main tutorial](../main-tutorial/00-introduction.md) first.
5+
6+
> [!NOTE]
7+
> This tutorial shows how to use [`postcss`](https://github.com/postcss/postcss) and its plugins to update `*.css` files.
8+
9+
Currently, `@codemod-utils` doesn't provide a utility package to handle CSS. This is because (1) a few different libraries can be used, each with pros and cons, and (2) most codemods for Ember projects concern updating `*.{hbs,js,ts}` files (more recently, `*.{gjs,gts}`).
10+
11+
Nonetheless, you can write a codemod to update many CSS files. This tutorial will show how to integrate PostCSS with `@codemod-utils/files`. Our target project is assumed to be an Ember app with CSS modules.
12+
13+
14+
## Table of contents
15+
16+
1. [Use existing plugins](./01-use-existing-plugins.md)
17+
1. [Write custom plugins](./02-write-custom-plugins.md)
18+
1. [Conclusion](./03-conclusion.md)
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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

Comments
 (0)