preface

While projects built on Vite are very fast, running old projects on Vite can take a lot of time. For example, some react projects require the use of.jsx as the suffix for the React syntax sugar file, while others use.js as the suffix. Either babel-loader or esbuild-Loader is used for JSX processing. However, Vite does not actually support.js ending JSX files, so in this case we had to write a Vite plugin to fit our previous project. This article will complete a plug-in that allows any CSS file to be modularized (ps: vite only allows.module.xxxx to be modularized).

Establish train of thought

The first thing we need to decide is what our end result is going to be.

We need to add the following code:

.container {
 color: red;
}
Copy the code

Convert to the code shown above.

We chose the common CSS processor PostCSS to accomplish this task. Of course, postCSS alone is not enough, but it also needs to be equipped with its plug-ins. In this case, we select the postCSs-Modules plugin.

Postcss-modules can help us add the following code

.title {
  color: green;
}

.article {
  font-size: 16px;
}
Copy the code

into

._title_xkpkl_5 {
  color: green;
}

._article_xkpkl_10 {
  font-size: 16px;
}
Copy the code

In addition, we can also get the mapping of the transformed class

{
  "title": "_title_xkpkl_5"."article": "_article_xkpkl_10"
}
Copy the code

Only with this mapping can our following code take effect. It uses style.title to retrieve the corresponding class name and then goes to the stylesheet to match the corresponding style.

  <div className={style.title}></div>
Copy the code

Once we have this useful information, we need to organize it and update it to the corresponding file. Vite provides a post-processing plug-in for us, and we just need to return the result.

Writing a front plug-in

The following is a standard template for a front-loaded plugin. Generally speaking, we only need to focus on the contents of the Transform. The first parameter raw represents the code inside the file, and the ID is the absolute path to the file.

export default function viteTransformCSSModulesPlugin() {
 const name = 'vite-plugin-transform-css-modules';
 return {
   enforce: 'pre',
   name,
   async transform(raw, id){}}; }Copy the code

Not every file is modularized by the PostCSS processor. So we need to write some conditions to filter it out. For example, we don’t need to deal with.module. SCSS files, we don’t need to deal with non-CSS files, we don’t need to deal with files under node_modules.

const cssLangs = `\\.(scss|styl|stylus|pcss|postcss)($|\\?) `;
const cssLangRE = new RegExp(cssLangs);
const cssModuleRE = new RegExp(`\\.module${cssLangs}`);

export default function viteTransformCSSModulesPlugin() {
  const name = 'vite-plugin-transform-css-modules';
  return {
    enforce: 'pre',
    name,
    async transform(raw, id) {
      if(cssLangRE.test(id) && ! id.includes('node_modules') && !cssModuleRE.test(id)) {
      
      }
    }
  };
}
Copy the code

Next, we need to write a function to get the CSS code and module mapping.

 const { code: css, modules } = await compileCSS(id, raw);
Copy the code

Through the postcss – modules after processing, we can gain from postcssResult. CSS gets processed CSS, through getJSON function can access to the map mapping relation.

async function compileCSS(id, code) {
  let modules;
  let postcssPlugins = [];
  postcssPlugins.unshift(
    (await import('postcss-modules')).default({ ... modulesOptions,getJSON(cssFileName, _modules, outputFileName) {
        modules = _modules;
        if (modulesOptions && typeof modulesOptions.getJSON === 'function') { modulesOptions.getJSON(cssFileName, _modules, outputFileName); }}}));const postcssResult = await (await import('postcss')).default(postcssPlugins).process(code, {
    to: id,
    from: id,
    map: {
      inline: false.annotation: false}});return {
    ast: postcssResult,
    modules,
    code: postcssResult.css
  };
}
Copy the code

We can look at the output.

 console.log('Processed CSS code -->\n', css);
 console.log('Mapping obtained after processing -->\n', modules);
Copy the code

Next, we need to convert our data to ESModule data via dataToEsm.

import { dataToEsm } from '@rollup/pluginutils';
const modulesCode = dataToEsm(modules, { namedExports: true.preferConst: true });
console.log('dataToEsm '-->\n', modulesCode);
Copy the code

So far our pre-plug-in has been handled. The complete code is as follows:

export default function viteTransformCSSModulesPlugin() {
  const name = 'vite-plugin-transform-css-modules';
  return {
    enforce: 'pre',
    name,
    async transform(raw, id) {
      if(cssLangRE.test(id) && ! id.includes('node_modules') && !cssModuleRE.test(id)) {
        // Get the modular CSS source code and the modular object name
        const { code: css, modules } = await compileCSS(id, raw);
        // Convert modular objects to strings using dataToEsm
        const modulesCode =
          modules && dataToEsm(modules, { namedExports: true.preferConst: true });
        // Export the string after the module to be used by the postscript plug-in
        exportModules = modulesCode;
        return {
          code: css,
          map: { mappings: ' '}}; }return undefined; }}; }Copy the code

Write a post-plug-in

The definition of the post-processing plug-in differs from that of the pre-processor by using the Enforce parameter, and the CSS in the Transform callback parameter is the result returned by our pre-processor.

export default function viteTransformCSSModulesPluginPost() {
  const name = 'vite-plugin-transform-css-modules-post';
  return {
    enforce: 'post',
    name,
    async transform(css, id){}}; }Copy the code

We can capture CSS results by simple interception operations.

    const startStr = 'const css = ';
    const cssCodeStartIndex = css.indexOf(startStr);
    const cssCodeEndIndex = css.indexOf('updateStyle(id, css)');
    const cssStr = css.slice(cssCodeStartIndex + startStr.length, cssCodeEndIndex);
Copy the code

In the pre-processing, we have obtained the ESModule encoding, which we can export to the post-processor using ESModule features, because the value exported by the ESModule is a reference.

  async transform(css, id) {
    if(cssLangRE.test(id) && ! id.includes('node_modules') && !cssModuleRE.test(id)) {
      let startStr = 'const css = ';
      const cssCodeStartIndex = css.indexOf(startStr);
      const cssCodeEndIndex = css.indexOf('updateStyle(id, css)');
      const cssStr = css.slice(cssCodeStartIndex + startStr.length, cssCodeEndIndex);
      const pathIdx  = id.indexOf('/src/');
      const str = id.slice(pathIdx, id.length);
      return [
        `import.meta.hot = __vite__createHotContext('${str}'); `.`import { updateStyle, removeStyle } from "/@vite/client"`.`const id = The ${JSON.stringify(id)}`.`const css = ${cssStr}`.`updateStyle(id, css)`.// css modules exports change on edit so it can't self accept
        `${exportModules || `import.meta.hot.accept()\nexport default css`}`.`import.meta.hot.prune(() => removeStyle(id))`
      ].join('\n')}return undefined;
  },
Copy the code

So with a post-processor, we’re done. No longer limited to the module.XXX set by Vite.

JSX syntax sugar at the end of.js is supported

As we mentioned earlier, Vite does not support.js ending JSX syntactic sugar. The author also said why not:

The reason Vite requires the. JSX extension for JSX processing is that, in most cases, pure JS files work in browsers without the need for full AST transformations. Allowing JSX in.js files means that every supplied file must be fully AST processed in case it contains JSX.

Vite’s main maintainer also suggests that we might as well change the suffix name. Probably because most of the developers in the community didn’t think it was a good idea to change the file name, vite allowed us to write our own esbuild plugin

So we can configure the following code in vite.config.js:

import fs from 'fs/promises'; .optimizeDeps: {
  esbuildOptions: {
    plugins: [{name: "load-js-files-as-jsx".setup(build) {
          build.onLoad({ filter: /src\/.*\.js$/ }, async (args) => {
            return ({
              loader: "jsx".contents: await fs.readFile(args.path, "utf8"),})}); },},],},},Copy the code

Write in the last

The example in this article has been uploaded to github repository for easy debugging