Webpack 导入顺序在文件中的代码和文件夹中的代码之间创建阴影

Webpack import order creating shadowing between code in a file and code in a folder

我们有时会遇到这样的情况:

import { foo, bar } from '../../services/blaService';

我们同时拥有文件 blaService.ts 和文件夹 blaService/index.ts

Webpack 首先加载文件并丢弃文件夹中的代码,这是预期的行为。

我们是否可以通过在发生此类代码隐藏情况时抛出错误来防止这种情况发生?

TLDR;

这里有一个解决方法:

webpack.config.js

const path = require('path');
const fs = require('fs');

const DetectShadowingPlugin = {
  apply (resolver) {
    const beforeResolved = resolver.getHook('before-resolved');

    // `cb`- refers to the next `tap` function in the chain
    beforeResolved.tapAsync('DetectShadowingPlugin', (req, ctx, cb) => {
      // To inspect the hook's chain until this moment, see `ctx.stack`(from top to bottom)
      // console.log(ctx);
      
      // The `path` will give us the full path for the file we're looking for
      const { path: filePath } = req;
      
      const ext = path.extname(filePath);
      if (ext !== '.js') {
        // Continuing the process
        return cb();
      }

      const fileName = path.basename(filePath, path.extname(filePath)); // 
      const possibleDirectoryPath = path.resolve(filePath, '..', fileName);
      fs.access(possibleDirectoryPath, err => {
        if (!err) {
          const message = `Apart from the file ${filePath}, there is also a directory ${possibleDirectoryPath}`;
          cb(new Error(message));
          
          return;
        }

        cb();
      });
    });
  },
};

/**
 * @type {import("webpack/types").Configuration}
 */
const config = {
  /* ... */

  resolve: {
    plugins: [DetectShadowingPlugin]
  },
};

module.exports = config;

结果:

文件结构如下:

├── deps
│   ├── foo
│   │   └── index.js
│   └── foo.js
├── dist
│   └── main.js
├── index.js
└── webpack.config.js

foo是这样导入的:

import defaultFooFn from './deps/foo';

如果你想尝试上面的例子,你可以查看this Github repo。稍后我会在 repo 的自述文件中添加设置细节(肯定.. 稍后 :D),但在那之前,这里是步骤:

  • git clone --recurse-submodules
  • cd webpack
    • 纱线
    • 纱线link
  • CD ..
  • 纱线link网络包
  • yarn 理解 - 还要检查 package.json 的脚本以获取更多相关信息

说明

webpack 使用 resolver 来查找文件的位置。我将此 发现过程 视为分支分支的集合。有点像 git 分支。它有一个起点,并根据某些条件选择到达终点的路径。

如果您复制了我在上一节中 link 编辑的存储库,您应该会在 webpack 文件夹中看到 webpack 存储库。如果您想更好地可视化这些 分支 的选择,您可以打开 webpack/node_modules/enhanced-resolve/lib/ResolverFactory.js 文件。您不必了解发生了什么,只需注意步骤之间的联系即可:

如您所见,parsed-resolve 作为参数出现在第一个位置和最后一个位置。你也可以看到它使用了各种插件,但它们有一个共同点:一般来说,第一个字符串是 source 最后一个字符串是 target 。我之前提到过,这个过程可以看作是分支的分支。好吧,这些分支是由节点组成的(直观地说),其中一个节点在技术上称为钩子。

起点是 resolve 钩子(来自 for 循环)。它之后的下一个节点是 parsed-resolve(它是 resolve 挂钩的 目标 )。 parsed-resolve 钩子的目标是 described-resolve 钩子。等等。

现在,有一件重要的事情要提一下。您可能已经注意到,described-resolve 挂钩被多次用作 source。每次发生这种情况时,都会添加一个新步骤(技术上称为 tap)。从一个节点移动到另一个节点时,将使用这些步骤。如果该插件(插件添加了一个步骤)决定如此(这可能是插件中满足某些条件的结果),那么您可以从一个步骤走另一条路。

所以,如果你有这样的事情:

plugins.push(new Plugin1("described-resolve", "another-target-1"));
plugins.push(new Plugin2("described-resolve", "another-target-1"));
plugins.push(new Plugin3("described-resolve", "another-target-2"));

described-resolve 您可以通过 2 步到达 another-target-1(因此有 2 种方法可以到达)。如果插件中的一个条件不满足,它将转到下一个条件,直到满足插件的条件。如果根本没有选择 another-target-1,那么 Plugin3 的条件可能会导致 another-target-2

所以,就我的观点而言,这就是这个过程背后的逻辑。在这个过程中的某个地方,有一个挂钩(或节点,如果我们坚持最初的类比),它在成功找到 文件 后被调用。这是 resolved 钩子,也代表流程的最后一部分。
如果达到这一点,我们就可以确定文件存在。我们现在可以做的是检查是否存在同名文件夹。这就是这个自定义插件正在做的事情:

const DetectShadowingPlugin = {
  apply (resolver) {
    const beforeResolved = resolver.getHook('before-resolved');

    beforeResolved.tapAsync('DetectShadowingPlugin', (req, ctx, cb) => {
      const { path: filePath } = req;
      
      const ext = path.extname(filePath);
      if (ext !== '.js') {
        return cb();
      }

      const possibleDirectoryPath = path.resolve(filePath, '..', fileName);
      fs.access(possibleDirectoryPath, err => {
        if (!err) {
          const message = `Apart from the file ${filePath}, there is also a directory ${possibleDirectoryPath}`;
          cb(new Error(message));
          
          return;
        }

        cb();
      });
    });
  },
};

这里有一个有趣的实现细节,就是before-resolved。请记住,每个挂钩,为了确定其新目标,它必须经历一些由使用相同源的插件定义的条件。我们在这里做类似的事情,除了我们告诉 webpack 运行 我们的自定义条件 first。我们可以说它增加了一些优先级。如果我们想 运行 这是最后一个条件,我们会用 after.

替换 before

为什么它首先选择 requestName.js 路径而不是 requestName/index.js

这是因为内置插件的添加顺序。如果您在 ResolverFactory 中向下滚动一点,您应该到达这些行:

// The `requestName.js` will be chosen first!
plugins.push(
    new ConditionalPlugin(
        "described-relative",
        { directory: false },
        null,
        true,
        "raw-file"
    )
);

// If a successful path was found, there is no way of turning back.
// So if the above way is alright, this plugin's condition won't be invoked.
plugins.push(
    new ConditionalPlugin(
        "described-relative",
        { fullySpecified: false },
        "as directory",
        true,
        "directory"
    )
);

您可以通过注释掉上面的 raw-file 插件来测试它:

然后,根据 repo,您​​应该会看到类似的内容,表明已选择:

您还可以在该工作树中的任意位置放置断点,然后按 F5 来检查程序的执行情况。一切都在 launch.json 文件中。