DEV Community

Cover image for Css Module Type + Nextjs 14!
Reza Moosavi
Reza Moosavi

Posted on

Css Module Type + Nextjs 14!

In this article

Introduction

In this upcoming article, we're diving into using CSS Modules with Webpack5, Nextjs, and TypeScript. Following my exploration of Vite in this link, our goal is to assist developers in handling TypeScript errors tied to CSS Modules and share practical tips on styling with style.module.css.

We aim to demystify the process, making it easy for developers to ensure type safety in their styles and write more robust, maintainable code.

By the end of this article, you'll have a clear understanding of managing TypeScript errors in the context of CSS Modules, empowering you to create clean, error-free code in your Nextjs projects.

Whether you're a TypeScript enthusiast or a React developer seeking better styling practices, join us to simplify TypeScript errors and highlight best practices when working with CSS Modules in the webpack environment.

Time-saving Webpack plugin

Automating the generation of module types for CSS Modules is a game-changer. This tool simplifies the entire process, minimizing manual work and decreasing the likelihood of errors. Specifically designed for Next.js with Webpack, it seamlessly integrates into your projects, effortlessly managing CSS Module type definitions. Whether you're an experienced developer or just diving into Next.js and Webpack, this plugin is your go-to for streamlining the workflow related to CSS Modules in TypeScript. It becomes an indispensable asset for enhancing the development experience in your React projects within the Next.js and Webpack environment.

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: As noted in this article for Vite 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 Webpack 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.js in the root of your project.

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

//watching-css-modules.ts

import { Plugin } from "vite";

class CssModuleTypes {
  constructor(rootDir = "./src/") {
    this.rootDir = rootDir ?? "";
  }
  apply(compiler) {
    compiler.hooks.emit.tapAsync("TranslatePlugin", (compilation, callback) => {
      compilation.contextDependencies.add(
        path.resolve(__dirname, this.rootDir)
      );
      callback();
    });
    compiler.hooks.invalid.tap("Invalid", (fileName, changeTime) => {
      //code will be added later
    });
  }
}

function withCssTypes(nextConfig = {}) {
  return Object.assign({}, nextConfig, {
    webpack: (config) => {
      config.plugins.push(new CssModuleTypes());
      return config;
    },
  });
}

module.exports = withCssTypes;

Enter fullscreen mode Exit fullscreen mode

about above code:

The webpack Compiler supports watching which monitors the file system and recompiles as files change. When in watch mode, the compiler will emit the additional events such as watchRun, watchClose, and invalid.
I used invalid here because two parameters are passed to it: one is fileName, and the other is changeTime. The fileName is necessary for checking whether the module.css file has changed.

Now, before proceeding with the development of our plugin, it's necessary to add it to webpack. Therefore, add the css-modules-types plugin to next.config.js:

//next.config.js

const withCssType = require("./watching-css-modules");

/** @type {import('next').NextConfig} */
const nextConfig = {};

module.exports = withCssType(nextConfig);


Enter fullscreen mode Exit fullscreen mode

To locate all module.css files during app runtime and track changes in development, two packages, "path" and "fs," are employed. After filtering and finding module.css files, their content is extracted. Utilizing postCss, we then identify all classes used within these files. Subsequently, corresponding types for the found classes are generated. Finally, employing the "prettier" package, we format their content and save them in files with the same names as the source module.css files, appended with the .d.ts extension.

const fs = require("fs");
const postcss = require("postcss");
const selectorParse = require("postcss-selector-parser");
const prettier = require("prettier");
const path = require("path");
const srcDir = path.join(process.cwd(), "src");

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

  for (const str of arr) {
    if (!uniqueArray.includes(str)) {
      uniqueArray.push(str);
    }
  }
  return uniqueArray;
};
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 typeDeceleration = async (classArray) => {
  const data = `declare const styles: {${classArray
    ?.map((el, index) => `readonly '${el}': string;\n`)
    .join("")}};export default styles;`;
  const formattedData = await prettier.format(data, {
    parser: "typescript",
  });
  return formattedData;
};

function isDir(dir) {
  try {
    return fs.statSync(dir).isDirectory();
  } catch {
    return false;
  }
}
function createUniquesClassName(fullPath) {
  return new Promise((resolve, reject) => {
    let 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, (error) =>
        console.log("error in writeFileSync:", error)
      );
    } catch (err) {
      console.log("error in writing file:", err);
    }
  }
}

function getCssModulesFiles(pathDir) {
  let directory = pathDir ?? srcDir;

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

class CssModuleTypes {
  constructor(rootDir = "./src/") {
    this.rootDir = rootDir ?? "";
  }
  apply(compiler) {
    compiler.hooks.emit.tapAsync("TranslatePlugin", (compilation, callback) => {
      compilation.contextDependencies.add(
        path.resolve(__dirname, this.rootDir)
      );
      callback();
    });
    compiler.hooks.invalid.tap("Invalid", (fileName, changeTime) => {
      getCssModulesFiles(fileName);
    });
  }
}

function withCssTypes(nextConfig = {}) {
  return Object.assign({}, nextConfig, {
    webpack: (config) => {
      config.plugins.push(new CssModuleTypes());
      return config;
    },
  });
}

module.exports = withCssTypes;

Enter fullscreen mode Exit fullscreen mode

Conclusion

With this plugin, you can safely leverage the benefits of CSS Modules and TypeScript. Initially planned as a standalone package, I chose to share it through an article format, providing the complete source code on GitHub. Dive into the article and explore the code to spark ideas for your own plugins.
In another article, discover this plugin adapted for Vite and React. Check out this link where we discuss Vite, React, and CSS Modules.

Top comments (0)