DEV Community

Cover image for Css Module Type + Vite!
Reza Moosavi
Reza Moosavi

Posted on

Css Module Type + Vite!

In this article

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

//tsconfig.node.json

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

Enter fullscreen mode Exit fullscreen mode

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

//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
    },
  };
}

Enter fullscreen mode Exit fullscreen mode

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:

//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()],
});

Enter fullscreen mode Exit fullscreen mode

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:

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));
        });
      }
    },
  };
}

Enter fullscreen mode Exit fullscreen mode

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 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, where we discuss webpack, Next.js 14, and CSS Modules.

Top comments (0)