Css Module Type + Vite!
Tags: react, vite, css, typescript
Published: 2024-01-19 16:31:08


## In this article
- [In this article](#in-this-article)
- [Introduction](#introduction)
- [Time-saving Vite plugin](#introducing-a-time-saving-vite-plugin) 
- [Steps](#steps)
- [Start Create __Vite plugin__](#Start-Create-Vite-plugin)
- [Conclusion](#conclusion)
- [Source Code](https://github.com/rezamoosavidweb/vite-css-module-type)




## Introduction
In this article, we're focusing on making CSS Modules in React with Vite and TypeScript more approachable. Our aim is to help developers tackle TypeScript errors related to CSS Modules and share practical best practices when importing styles from style.module.css.

We want to demystify the process of handling TypeScript errors, making it easy for developers to ensure type safety in their styles. By understanding different types of CSS Modules, you'll be equipped to write more robust and maintainable code.


By the end of this article, you'll have a solid grasp of handling TypeScript errors in the context of CSS Modules, empowering you to write clean, error-free code in your React projects built with Vite.

Whether you're a TypeScript enthusiast exploring CSS Modules or a React developer seeking better styling practices, this guide is designed to offer insights and solutions to common challenges. Join us as we simplify TypeScript errors and highlight best practices when working with CSS Modules in the React and Vite environment.


## Time-saving Vite plugin
automating the creation of module types for CSS Modules. This tool streamlines type generation, eliminating manual efforts and reducing errors. Integrated seamlessly into Vite projects, it handles CSS Module type definitions automatically, providing a smoother development experience. Whether you're a seasoned developer or new to Vite and TypeScript, this plugin simplifies the workflow for CSS Modules in TypeScript, making it an essential tool for React projects.

## Steps
1)Identify all CSS modules.

2)Extract classes defined within them./

3)Create a file with a .d.ts extension alongside each module.css, 
placing type definitions for classes based on the found classNames.

4)Format the file content using Prettier ,finally save the file.

5)All the above steps should be performed for each change in CSS module files.

__Note:__ These steps are performed during development so that types are generated once when the app is run. Afterward, when the app is running, each modified __css module__ file is checked, and its type is generated accordingly.

## Start Create __Vite plugin__
Before you start, you need to have a React app with Vite and TypeScript, which you can download from [__this link__](https://stackblitz.com/edit/vitejs-vite-qhjhjj?file=index.html&terminal=dev). 
Then, create a file named __watching-css-modules.ts__ in the root of your project and place this file in __tsconfig.node.json__ as follows:
```bash
//tsconfig.node.json

{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts", "watching-css-modules.ts"]
}

```
now add blew to watching-css-modules.ts file:
```bash
//watching-css-modules.ts

import { Plugin } from "vite";

export function CssModuleTypes(): Plugin {
  return {
    name: "css-modules-types",
    apply: "serve",
    async configureServer() {
      //code will be added later
    },
    // HMR
    async handleHotUpdate({ server: { config }, file }) {
      //code will be added later
    },
  };
}

```
### about above code:
* Each Vite plugin needs a __name__ for identification in logs during errors
* the value of __apply:server__ specifies when this plugin should run (server/build)
*  The __configureServer__ hook runs only when the server is started, checking all module.css files once and generating their types.
* The __handleHotUpdate__ hook runs whenever any file changes, creating corresponding types for any changes in module.css files.


Now, before proceeding with the development of our plugin, it's necessary to introduce it to Vite. Therefore, add the css-modules-types plugin to vite.config.ts:
```bash
//vite.config.ts

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { CssModuleTypes } from "./watching-css-modules";

export default defineConfig({
  plugins: [react(), CssModuleTypes()],
});

```

In the __configureServer__ hook, locate all files with the .module.css extension and, using postcss, extract the used classes. Additionally, in the __handleHotUpdate__ hook, which runs for every file change, if the changed file has a .module.css extension, extract the classes using postcss and generate the TypeScript type file based on them. You can find the final watching-css-modules.ts file below:

```bash
import fs from "fs";
import path from "path";
import postcss from "postcss";
import selectorParse from "postcss-selector-parser";
import prettier from "prettier";
import { Plugin, ResolvedConfig } from "vite";

function isDir(dir) {
  try {
    return fs.statSync(dir).isDirectory();
  } catch {
    return false;
  }
}

function isCSSSelectorValid(selector) {
  try {
    selectorParse().processSync(selector);
    return true; // If no errors occurred, the selector is valid
  } catch (error) {
    console.error(`Invalid CSS selector: ${selector}`);
    return false; // If an error occurred, the selector is not valid
  }
}
const changingFilePath = (config: ResolvedConfig, file: string): string =>
  path.join(config.build.outDir, path.relative(config.publicDir, file));

const removeDupStrFromArray = (arr: string[]): string[] => {
  const uniqueArray = [];

  for (const str of arr) {
    if (!uniqueArray.includes(str)) {
      uniqueArray.push(str);
    }
  }

  return uniqueArray;
};

const typeDeceleration = async (classArray: string[]) => {
  const data = `declare const styles: {${classArray
    ?.map((el) => `readonly '${el}': string;`)
    .join("")}};export default styles;`;
  const formattedData = await prettier.format(data, {
    parser: "typescript",
  });
  return formattedData;
};

function createUniquesClassName(fullPath): Promise<string[]> {
  return new Promise((resolve, reject) => {
    const css = fs.readFileSync(fullPath);
    const classNames = [];
    postcss()
      .process(css, { from: fullPath, to: fullPath?.replace(".css", ".d.css") })
      .then(async (result) => {
        result.root.walkRules((rule) => {
          if (!isCSSSelectorValid(rule.selector)) return;
          selectorParse((selectors) => {
            selectors.walkClasses((selector) => {
              classNames.push(selector.value);
            });
          }).process(rule.selector);
        });

        const uniquesClassName = await removeDupStrFromArray(classNames);
        resolve(uniquesClassName);
      })
      .catch(reject);
  });
}
async function createDecelerationFile(fullPath) {
  const uniquesClassName = await createUniquesClassName(fullPath);

  if (uniquesClassName?.length > 0) {
    const decelerationPath = fullPath?.replace(
      ".module.css",
      ".module.css.d.ts"
    );
    const formattedDeceleration = await typeDeceleration(uniquesClassName);

    try {
      fs.writeFileSync(decelerationPath, formattedDeceleration);
    } catch (err) {
      console.log("error in writing file:", err);
    }
  }
}
function getCssModulesFiles(pathDir) {
  const directory = pathDir;

  if (isDir(directory)) {
    fs.readdirSync(directory).forEach(async (dir) => {
      const fullPath = path.join(directory, dir);
      if (isDir(fullPath)) return getCssModulesFiles(fullPath);
      if (!fullPath.endsWith(".module.css")) return;

      try {
        createDecelerationFile(fullPath);
      } catch (e) {
        console.log(e);
      }
    });
  } else {
    if (!directory.endsWith(".module.css")) return;
    createDecelerationFile(directory);
  }
}

export function CssModuleTypes(): Plugin {
  return {
    name: "css-modules-types",
    apply: "serve",
    async configureServer() {
      const directory = path.join(__dirname, "./src");
      await getCssModulesFiles(directory);
    },
    // HMR
    async handleHotUpdate({ server: { config }, file }) {
      if (file.endsWith("module.css")) {
        fs.readFile(changingFilePath(config, file), "utf8", (err, css) => {
          postcss()
            .process(css, {
              from: changingFilePath(config, file),
            })
            .then(async (result) => {
              const classNames = [];
              try {
                result.root.walkRules((rule) => {
                  if (!isCSSSelectorValid(rule.selector)) return;
                  selectorParse((selectors) => {
                    selectors.walkClasses((selector) => {
                      classNames.push(selector.value);
                    });
                  }).process(rule.selector);
                });

                const uniquesClassName = removeDupStrFromArray(classNames);

                if (uniquesClassName?.length > 0) {
                  const newDestPath = changingFilePath(config, file)?.replace(
                    ".module.css",
                    ".module.css.d.ts"
                  );
                  fs.writeFile(
                    newDestPath,
                    await typeDeceleration(uniquesClassName),
                    (error) => console.log("error:", error)
                  );
                }
              } catch (error) {
                console.log(`error in ${result.opts.from}:`, error);
              }
            })
            .catch((err) => console.log(`error in css file:`, err));
        });
      }
    },
  };
}

```
## Conclusion
with this plugin, you can harness the power of CSS Modules and TypeScript safely. Initially, I intended to publish this plugin as a package, but I decided to present it in the format of an article and provide its complete [__source code__](https://github.com/rezamoosavidweb/vite-css-module-type) on GitHub. By examining the article and exploring the full source code of this plugin, you can implement your ideas for your own plugins. 
In another article, you can find this plugin adapted for webpack. You can view that article at [__this link__](https://dev.to/rezamoosavidweb/css-module-types-nextjs-14-53go), where we discuss __webpack__, __Next.js 14__, and __CSS Modules__.