postcss-pxtorem icon indicating copy to clipboard operation
postcss-pxtorem copied to clipboard

[Bug Report: 6.0] Nested components and views may cause rootValue miscalculated (root in Once Event and decl in Declaration Event may be different)

Open zhaojjiang opened this issue 2 years ago • 5 comments

When project is complex enough, there will be nested views and components.

The event order can be Parent-Once --> Children-Once --> Children-Declaration --> Parent-Declaration, which result the rootValue miscalculated from function and so is rem.

I tried many times, and this problem happened many times, but I cannot provide a minimum code to stably reproduce it.

I have noticed there is runtime-defined listeners prepare in postcss may help solve this problem, and tried in another private project which can reproduce this problem. It works. The code like below. Move events wrapped by prepare().

I don't if this logic is right, so just leave it here. In my project, I will just downgrade package to 5.x in a short peried of time to avoid this problem.

module.exports = (options = {}) => {
  convertLegacyOptions(options);
  const opts = Object.assign({}, defaults, options);
  const satisfyPropList = createPropListMatcher(opts.propList);
  const exclude = opts.exclude;

  return {
    postcssPlugin: "postcss-pxtorem",
    prepare(result) {
      // console.log(result);

      let isExcludeFile = false;
      let pxReplace;

      return {
        Once(css) {
          // console.log('once--------', css.source.input.file);

          const filePath = css.source.input.file;
          if (
            exclude &&
            ((type.isFunction(exclude) && exclude(filePath)) ||
              (type.isString(exclude) && filePath.indexOf(exclude) !== -1) ||
              filePath.match(exclude) !== null)
          ) {
            isExcludeFile = true;
          } else {
            isExcludeFile = false;
          }

          const rootValue =
            typeof opts.rootValue === "function"
              ? opts.rootValue(css.source.input)
              : opts.rootValue;
          // console.log(css.source.input.file, rootValue);

          pxReplace = createPxReplace(
            rootValue,
            opts.unitPrecision,
            opts.minPixelValue
          );
        },
        Declaration(decl) {
          // console.log('decl', decl.source.input.file);
          if (isExcludeFile) return;

          if (
            decl.value.indexOf("px") === -1 ||
            !satisfyPropList(decl.prop) ||
            blacklistedSelector(opts.selectorBlackList, decl.parent.selector)
          )
            return;

          const value = decl.value.replace(pxRegex, pxReplace);

          // if rem unit already exists, do not add or replace
          if (declarationExists(decl.parent, decl.prop, value)) return;

          if (opts.replace) {
            decl.value = value;
          } else {
            decl.cloneAfter({ value: value });
          }
        },
        AtRule(atRule) {
          if (isExcludeFile) return;

          if (opts.mediaQuery && atRule.name === "media") {
            if (atRule.params.indexOf("px") === -1) return;
            atRule.params = atRule.params.replace(pxRegex, pxReplace);
          }
        }
      }
    },
  };
};

zhaojjiang avatar Nov 25 '23 10:11 zhaojjiang

additional info: this problem happens only when reuse postcss or postcss-loader (I am not sure how to describe this concept)

Test code:

// ...

let pxReplace;
let testInc = 0; // add line
return {
  postcssPlugin: "poxtcss-pxtorem",
  Once(css) {
    console.log('inc---------------', testInc); // add console
    testInc++; // add line

   // ...
  },
  // ...
};
// ...

the console in vue by webpack & vue-cli is always 0, but increased to 1, 2, 3, ... in vue by vite & rollup

zhaojjiang avatar Nov 26 '23 06:11 zhaojjiang

I encountered the same issue. After some debugging, I identified the root cause. The Once method executes when traversing each file, while Declaration runs when traversing CSS properties within a file. Normally, the execution order should be: File A Once -> File A n x Declaration -> File B Once -> File B n x Declaration. However, both Once and Declaration are designed to be executed asynchronously in PostCSS, which can lead to scenarios like: File A Once -> File B Once -> File A n x Declaration -> File B n x Declaration. This is the core reason behind this issue. How does this cause incorrect rootValue retrieval? It's because pxReplace is defined in the plugin's root scope. Code in the root scope executes during plugin initialization, meaning the same pxReplace instance is used across all file traversals. Consequently, the unpredictable out-of-order execution of Once leads to unexpected overwrites of pxReplace, ultimately resulting in incorrect rootValue. Asynchrony in PostCSS is intended for higher execution efficiency and isn't inherently problematic. Plugins should be aware of this and avoid issues stemming from it. Your approach is correct: reviewing the source code shows that prepare executes when retrieving the plugin object during file traversal. By placing pxReplace in the appropriate scope, Declaration will always use the correct pxReplace instance. Additionally, an observation: this behavior can be described as both stable and unstable. It's stable in the sense that when your project's code is unchanged, the execution order remains consistent across compilations—if it's out of order, it stays out of order; if it's in order, it stays in order. It's unstable because changes to the project can cause unexpected shifts in this execution order, leading to issues. Another finding: when attempting to observe the out-of-order behavior in debug mode, it reverts to the correct order. Only through console.log can the out-of-order scenario be observed. This seems to relate to how Node.js handles asynchronous execution, and this elusive behavior makes it difficult for us to provide a minimal reproduction demo.

我同样遇到了这个问题。经过我一番debug,发现了问题所在。Once方法是在遍历每个文件时执行的,Declaration是在遍历文件内的css属性,正常来说是A文件 Once->A文件n x Declaration->B文件Once->B文件 n x Declaration按照这种顺序执行的。但是无论Once还是Declaration都被postcss设计成异步执行,这时就有可能出现A文件Once->B文件Once->A文件n x Declaration->B文件 n x Declaration这种情况,这就是导致此issue的核心原因。这个原因又是如何导致获取到错误的rootValue呢,这是因为pxReplace被定义在插件的根作用域,根作用域代码在初始化插件时执行,因而导致所有文件遍历时都会使用同一个pxReplace。所以正是不可预期的乱序Once执行导致pxReplace意外覆盖最终导致rootValue取值错误。 异步对于postcss来说是为了更高的执行效率,是没有问题的,所以插件应该对此有所了解并避免因此产生的问题。你的做法就是正确的,翻阅源码可以看见prepare 是在遍历文件时获取插件对象时执行的,pxReplace放在了正确的作用域内,这样Declaration使用的pxReplace总是正确的。 另外说一点发现,这个情况可以说是稳定的也可以说是不稳定的。他稳定在于当你项目处于一个代码不变状态时,无论编译多少次,执行顺序都是固定的,是乱序的就一直是乱序的,是正序的就一直是正序的;不稳定在于当你项目变化后,这个执行顺序就会出现意料之外的变化而导致问题。还有个发现,当你想通过debug模式去观察为何出现乱序时,他就会变成正确的顺序,也就是只有通过console.log的方式才能发现乱序的情况。这似乎要追溯到node对于异步的执行情况,也正是这种难以观察的情况导致我们都没法给出一个最小实现的demo。

morningbao avatar Sep 10 '25 06:09 morningbao

看起来这个作者已经很久没有写代码了,建议后面看到的人还是退到5.1版本解决问题。改动源码不便于其他成员安装项目,还有可能在生产发版时遗漏或者忘记。

morningbao avatar Sep 10 '25 07:09 morningbao