We can save time if there's already a PostCSS plugin that meets your needs.
As a concrete example, we'll use postcss-nested to remove nested code. We may want to do this for a few different reasons:
- We want to migrate away from Sass.
- We prefer native CSS while CSS nesting remains in spec.
- We want to remove a PostCSS plugin (our project has too many plugins).
- We use CSS modules (class selectors are hashed) so nesting isn't needed.
Change the directory to a place where you like to keep projects. Then, run these commands:
# Create project
npx @codemod-utils/cli write-native-css
# Install dependencies
cd write-native-css
pnpm install
# Install postcss and postcss-nested as dependencies
pnpm install postcss and postcss-nestedCreate a step called remove-css-nesting. It is to read *.css files and write back the file content (a no-op).
src/steps/remove-css-nesting.ts
For brevity, how src/index.ts calls removeCssNesting() is not shown.
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { createFiles, findFiles } from '@codemod-utils/files';
import type { Options } from '../types/index.js';
export function removeCssNesting(options: Options): void {
const { projectRoot } = options;
const filePaths = findFiles('app/**/*.css', {
projectRoot,
});
const fileMap = new Map(
filePaths.map((filePath) => {
const oldFile = readFileSync(join(projectRoot, filePath), 'utf8');
return [filePath, oldFile];
}),
);
createFiles(fileMap, options);
}To test the step, here's a stylesheet with nested code:
tests/fixtures/sample-project/input/app/components/ui/page.css
Note, the syntax @value is specific to CSS modules. We will later replace it with var() from native CSS.
@value (
desktop,
spacing-400,
spacing-600
) from "my-design-tokens";
@value navigation-menu-height: 3rem;
.container {
display: grid;
grid-template-areas:
"header"
"body";
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
height: calc(100% - navigation-menu-height);
overflow-y: auto;
padding: spacing-600 spacing-400;
scrollbar-gutter: stable;
.header {
grid-area: header;
}
.body {
grid-area: body;
}
@media desktop {
grid-template-areas:
"header body";
grid-template-columns: auto 1fr;
grid-template-rows: 1fr;
height: 100%;
}
}Next, we use the postcss-nested plugin to update the file.
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { createFiles, findFiles } from '@codemod-utils/files';
+ import postcss from 'postcss';
+ import PostcssNestedPlugin from 'postcss-nested';
import type { Options } from '../types/index.js';
+ function transform(file: string): string {
+ const plugins = [PostcssNestedPlugin()];
+
+ return postcss(plugins).process(file).css;
+ }
+
export function removeCssNesting(options: Options): void {
const { projectRoot } = options;
const filePaths = findFiles('app/**/*.css', {
projectRoot,
});
const fileMap = new Map(
filePaths.map((filePath) => {
const oldFile = readFileSync(join(projectRoot, filePath), 'utf8');
+ const newFile = transform(oldFile);
- return [filePath, oldFile];
+ return [filePath, newFile];
}),
);
createFiles(fileMap, options);
}Run ./update-test-fixtures.sh. You will see that .header, .body, and @media blocks are no longer inside the .container block.
tests/fixtures/sample-project/output/app/components/ui/page.css
@value (
desktop,
spacing-400,
spacing-600
) from "my-design-tokens";
@value navigation-menu-height: 3rem;
.container {
display: grid;
grid-template-areas:
"header"
"body";
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
height: calc(100% - navigation-menu-height);
overflow-y: auto;
padding: spacing-600 spacing-400;
scrollbar-gutter: stable;
}
.container .header {
grid-area: header;
}
.container .body {
grid-area: body;
}
@media desktop {
.container {
grid-template-areas:
"header body";
grid-template-columns: auto 1fr;
grid-template-rows: 1fr;
height: 100%
}
}Tip
Often, formatting can't be preserved. Ask the consuming project to use prettier and stylelint so that you can separate formatting concerns.