Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

记一次升级webpack 5引出的css modules 语法变更的自定义 eslint 插件 #1

Open
csu-feizao opened this issue Apr 29, 2022 · 0 comments

Comments

@csu-feizao
Copy link
Owner

csu-feizao commented Apr 29, 2022

前段时间对项目升级了 webpack5 后,发现 css-loader 中 css-modules 的配置有一些 breakchange。笔者为这些不兼容的语法开发了一个 eslint 插件来校验,源码见 eslint-plugin-css-modules-es

What changes

默认开启了 esModule

默认情况下,css-loader 生成使用 ES 模块语法的 JS 模块。 在某些情况下,使用 ES 模块是有益的,例如在模块串联或 tree shaking 时。

提供 namedExport 配置,值为 true 时,exportLocalsConvention 的值只能为 camelCaseOnly

⚠ 本地环境的命名将转换为驼峰格式,即 exportLocalsConvention 选项默认设置了 camelCaseOnly。

⚠ 如果你设置 namedExport 为 true 那么只有 camelCaseOnly 被允许。

⚠ 不允许在 CSS 类名中使用 JavaScript 保留字。

在老版本中,我们使用的是 camelCase: true 配置,我们先来看 camelCaseOnlycamelCase 的区别。

camelCase: 类名将被驼峰化,原类名不会从局部环境删除

camelCaseOnly: 类名将被驼峰化,原类名从局部环境删除

举例:

.delete_icon {
    font-size: 12px;
}

老版本编译出来的对象为:

{
    delete_icon: 'delete_icon__xxxx',
    deleteIcon: 'delete_icon__xxxx',
    default: {
        delete_icon: 'delete_icon__xxxx',
        deleteIcon: 'delete_icon__xxxx',
    }
}

新版本编译出来的对象为:

{
    default: undefined,
    deleteIcon: 'delete_icon__xxxx',
    __esModule: true
}

可以看出,原先代码中的几个用法,在升级后均会出现问题

// there is no default any more
import style from './style.module.css' 
import * as style from './style.module.css'

export default function() {
    // 原始值不存在了,只剩下驼峰化的值
    return (
        <>
          <div class={style.delete_icon}>demo</div>
          <div class={style['delete_icon']}>demo1</div>
        </>
    )
}

再来看 JavaScript 保留字的 breakchange。为什么不能使用保留字作为类目?我们来看下面的例子

.delete {
    color: red;
}
.default {
    color: blue;
}

在将上述代码编译成 es 模块时,相当于以下代码

export const delete = 'delete__xxxx'

export const defalut = 'default__xxxx'

在严格模式下,显然都是不行的

How to resolve

  1. 默认导入

我们要先让所有使用 css-modules 默认导入的代码都抛出错误,因此只要自定义一个 eslint 规则来校验即可,代码比较简单,这里就直接贴出来了

// no-default-export-css-modules.js
module.exports = {
    meta: {
      messages: {
        noDefaultExport: 'Do not use default export of css modules'
      }
    },
    create(context) {
      return {
        ImportDefaultSpecifier (node) {
          const isCssModule = isCssModulesFile(context, node.parent.source.value)
          if (isCssModule) {
            context.report({
              node,
              messageId: 'noDefaultImport'
            })
          }
        }
      }
    }
  }
  1. camelCaseOnly

跟默认导入的思路一样,我们可以自定义一个 eslint 规则,这里需要处理具名导入和命名空间导入两种情况

  • 具名导入
import { delete_icon } from './style.module.css'

如上,解析出来的 AST 对应为 ImportSpecifier,但要注意导入时可以重命名

import { delete_icon as deleteIcon } from './style.module.css'

在 AST 中 delete_icon 对应 node.importeddeleteIcon 对应 node.local,因此我们应该判断 node.imported 的值,而非 node.local

核心代码

function ImportSpecifier(node) {
  const isCssModule = isCssModulesFile(context, node.parent.source.value)
  if (!isCssModule) {
    return
  }
  if (!isCamelCase(node.imported.name)) {
    context.report({
      node,
      messageId: 'onlyCamelcaseProperty'
    })
  }
}
  • 命名空间导入
import * as style from './style.module.css'

console.log(style.delete_icon)

命名空间导入的校验相对复杂一点,因为我们需要找出所有引用命名空间变量的代码,判断是否有使用非驼峰化的类名。而且需要注意的是,如果原先类命名使用了破折号 -,那么使用的时候就不是 style.a-b,而是 style['a-b'],因此这两种逻辑都需要处理。(笔者项目上没有使用动态生成类名,如 style[a-${someVariable}],因此忽略这种情况)

核心代码

// only-camelcase-key-css-modules.js
function ImportNamespaceSpecifier(node) {
  const isCssModule = isCssModulesFile(context, node.parent.source.value)
  if (!isCssModule) {
    return
  }
  for (const variable of context.getDeclaredVariables(node)) {
    for (const reference of variable.references) {
      const idNode = reference.identifier
      const parentNode = idNode.parent
      if (!isMemberExpression(parentNode.type)) {
        return
      }
      const isObjectIdentifier = isIdentifier(parentNode.object.type)
      if (!isObjectIdentifier) {
        return
      }

      const propNode = parentNode.property

      // 处理 style.a_b 的场景
      const isPropIdentifier = isIdentifier(propNode.type)
      const propIdentifier = propNode.name

      // 处理 style['a-b'] 的场景
      const isPropLiteral = isLiteral(propNode.type)
      const propLiteral = propNode.value

      const isCamelcaseProp = (isPropIdentifier && isCamelCase(propIdentifier)) ||
        (isPropLiteral && isCamelCase(propLiteral))

      if ((isPropIdentifier || isPropLiteral) && !isCamelcaseProp) {
        context.report({
          loc: propNode.loc,
          messageId: 'onlyCamelcaseProperty'
        })
      }
    }
  }
}
  1. Javascript 保留字

若使用了保留字,webpack 编译模块的时候就会报错,因此就不需要自定义额外的 eslint 规则来校验。

运行结果:
eslint-demo
eslint-demo-result

结语

笔者第一次实践自定义 eslint,还是蛮有趣的。目前插件还比较简陋,没有实现自动修复功能,感兴趣的读者可以试试,欢迎贡献 pr。

后文

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant