esbuild混合插件以IIFE方式捆绑多个文件和单一的ESM捆绑。

huangapple go评论55阅读模式
英文:

esbuild hybrid plugin to bundle multiple files iife and single esm bundle

问题

以下是您要翻译的内容:

有一个项目SlickGrid,其中有多个文件都以iife方式编写(最初是使用jQuery命名空间作为iife构建的)。大多数文件都是可选的,用户可以选择加载相关的JavaScript功能文件(例如slick.contextmenu.jsslick.headermenu.js等),通过这样做,它将简单地扩展窗口对象上存在的Slick对象(基本上核心文件具有esbuild globalName: 'Slick'定义,其他文件只是在加载时扩展它,这不能进行树摇动优化,但通过仅加载所需的功能,可以保持构建大小较小)。

我想为仍然希望使用独立<script>加载的用户保持这些iife文件分开,但也希望为ESM(单个文件捆绑包)提供单独的构建文件夹。我认为可以使用esbuild编写一个esbuild插件来实现这一目标,方法是使用onResolve。我设法使其工作,但不够优雅,我希望能找到更好的解决方案。

import {
  Event as SlickEvent_,
  EventData as SlickEventData_,
  EditorLock as SlickEditorLock_,
  Utils as SlickUtils_,
} from './slick.core.js';
import { Draggable as SlickDraggable_, MouseWheel as SlickMouseWheel_, Resizable as SlickResizable_ } from './slick.interactions.js';

// TODO: 我希望避免写下面这些只对iife有用的行
// 对于iife,从window.Slick对象中提取,对于ESM使用命名导入
const SlickEvent = window.Slick ? Slick.Event : SlickEvent_;
const EventData = window.Slick ? Slick.EventData : SlickEventData_;
const EditorLock = window.Slick ? Slick.EditorLock : SlickEditorLock_;
const Utils = window.Slick ? Slick.Utils : SlickUtils_;
const Draggable = window.Slick ? Slick.Draggable : SlickDraggable_;
const MouseWheel = window.Slick ? Slick.MouseWheel : SlickMouseWheel_;
const Resizable = window.Slick ? Slick.Resizable : SlickResizable_;

// ...

// 然后在代码中正常使用它...
const options = Utils.extend(true, {}, defaults, options);

所以我写的自定义插件似乎有效,但有点不够优雅,它将使用window.Slick(如果找到)或使用ESM用于命名导入。对于ESM构建,将大致相同,不需要使用任何插件,因为我们希望将所有内容捆绑到一个单独的捆绑文件中,并保持命名导入,就像常规构建一样。

但是请注意,意图仍然是为iife构建生成多个文件,即使我们使用bundle :true,因为插件将简单地将任何导入替换为一个空字符串。

换句话说,插件只是从关联的window.Slick.featureXYZ加载代码,并将导入替换为一个空字符串,因为代码已经存在于window.Slick对象中,所以我们不需要再次使用导入的代码(这就是为什么我们将其替换为空字符串的原因)。

import { build } from 'esbuild';

const myPlugin = {
    name: 'my-plugin',
    setup(build) {
      build.onResolve({ filter: /.*/ }, args => {
        if (args.kind !== 'entry-point') {
          return { path: args.path + '.js', namespace: 'import-ns' }
        }
      })

      build.onLoad({ filter: /.*/, namespace: 'import-ns' }, (args) => {
        return {
          contents: `// 空字符串,什么都不做`,
          loader: 'js',
        };
      })
    }
};

build({
    entryPoints: ['slick.grid.js'],
    color: true,
    bundle: true,
    minify: false,
    target: 'es2015',
    sourcemap: false,
    logLevel: 'error',

    format: 'iife',
    // globalName: 'Slick', // 仅用于核心文件
    outfile: 'dist/iife/slick.grid.js',
    plugins: [myPlugin],
});

这种方法似乎有效,但不够优雅,理想情况下,如果我能获取命名导入并直接在代码中替换它们,而不必在导入后写所有这些额外的行,那将是很好的。我的代码库中(例如:const SlickEvent = window.Slick ? Slick.Event : SlickEvents;),这还将清除我的ESM构建中未使用的行(仅对iife构建有用)。

编辑

我找到了这个esbuild请求问题Request: Expose list of imports in onLoad/onResolve argument to allow custom tree-shaking,它正在寻找我所寻找的内容。该功能请求被拒绝,因为它可能在esbuild本身内部不可行,但提出了一些建议来查找命名导入,所以我将尝试一下。

英文:

There is a project SlickGrid that have multiple files that are all written as iife (it was originally built with jQuery namespace as iife). Most files are optional and the user can choose which feature they are interested (ie slick.contextmenu.js, slick.headermenu.js, ...) by loading the associated JavaScript feature file(s) and by doing so it will simply extend the Slick object that exist on the window object (basically the core file has an esbuild globalName: &#39;Slick&#39; defined and the other files are simply extending on it whenever they are loaded, it's not tree shakeable but it's a nice way to keep a build size small by only loading what feature they want).

I'd like to keep these iife file separate for the users who still want to use standalone &lt;script&gt; loading but also want to provide ESM (a single file bundle) in a separate build folder for ESM. I think that I can achieve this with esbuild by writing an esbuild plugin by using onResolve. I manage to make it work but it's not the most elegant, I'd like help to find a better solution

import {
  Event as SlickEvent_,
  EventData as SlickEventData_,
  EditorLock as SlickEditorLock_,
  Utils as SlickUtils_,
} from &#39;./slick.core.js&#39;;
import { Draggable as SlickDraggable_, MouseWheel as SlickMouseWheel_, Resizable as SlickResizable_ } from &#39;./slick.interactions.js&#39;;

// TODO: I would like to avoid having to write all of the following lines which are only useful for iife 
// for iife, pull from window.Slick object but for ESM use named import
const SlickEvent = window.Slick ? Slick.Event : SlickEvent_;
const EventData = window.Slick ? Slick.EventData : SlickEventData_;
const EditorLock = window.Slick ? Slick.EditorLock : SlickEditorLock_;
const Utils = window.Slick ? Slick.Utils : SlickUtils_;
const Draggable = window.Slick ? Slick.Draggable : SlickDraggable_;
const MouseWheel = window.Slick ? Slick.MouseWheel : SlickMouseWheel_;
const Resizable = window.Slick ? Slick.Resizable : SlickResizable_;

// ...

// then use it normally in the code...
const options = Utils.extend(true, {}, defaults, options);

So the custom plugin that I wrote seems to work, but it's a bit hacky, and it will use either window.Slick for iife (when found) OR use the named import for ESM usage. Running a build for ESM will be roughly the same but without using any plugin since we want to bundle everything into a single bundled file and keep named imports like a regular build.

However please note the intentation is to still produce multiple files for the iife build, that is even if we use bundle :true because the plugin will simple replace any of the imports with an empty string.

in other words, the plugin is simply loading the code from the associated window.Slick.featureXYZ and replaces the import with an empty string because the code exist in the window.Slick object already so we don't need to use the imported code again (hence why we replace that part with an empty string)

import { build } from &#39;esbuild&#39;;

const myPlugin = {
    name: &#39;my-plugin&#39;,
    setup(build) {
      build.onResolve({ filter: /.*/ }, args =&gt; {
        if (args.kind !== &#39;entry-point&#39;) {
          return { path: args.path + &#39;.js&#39;, namespace: &#39;import-ns&#39; }
        }
      })

      build.onLoad({ filter: /.*/, namespace: &#39;import-ns&#39; }, (args) =&gt; {
        return {
          contents: `// empty string, do nothing`,
          loader: &#39;js&#39;,
        };
      })
    }
};

build({
    entryPoints: [&#39;slick.grid.js&#39;],
    color: true,
    bundle: true,
    minify: false,
    target: &#39;es2015&#39;,
    sourcemap: false,
    logLevel: &#39;error&#39;,

    format: &#39;iife&#39;,
    // globalName: &#39;Slick&#39;, // only for the core file
    outfile: &#39;dist/iife/slick.grid.js&#39;,
    plugins: [myPlugin],
});

So this approach seems to work but is not very elegant, ideally it would be great if I could get the named imports and replace them directly in the code and avoid having to write all these extra lines after the imports in my codebase.

Does anyone have a better solution? Is there a way to get named imports in esbuild onResolve and onLoad?

So far what I found is that esbuild only provides the kind property as import-statement but it doesn't provide the named import that goes with it. If by any chance I could find how to get them, then I could maybe write my own code in the onLoad to override it with something like var Utils = window.Slick.${namedImport} for iife without having to write all these extra lines by myself in the codebase (ie: const SlickEvent = window.Slick ? Slick.Event : SlickEvents;), this would also cleanup these unused lines in my ESM build (it's only useful for the iife build).

EDIT

I found this esbuild request issue Request: Expose list of imports in onLoad/onResolve argument to allow custom tree-shaking which is asking for the same thing I was looking for. The feature request was rejected because it might not be possible within esbuild itself but a suggestion was posted to find the named imports, so I'll give that a try

答案1

得分: 0

为了回答自己的问题,经过一些尝试和错误,我提出了下面的代码。

首先,我保留了与原始问题中提到的相同的自定义插件,该插件在构建为 iife 时会删除所有的 import。该插件在下面的解决方案中显示出来(这与之前在问题中提到的代码相同)。

我的答案中的新内容,也就是缺失的部分,是使用 esbuild#define。这个建议来自我在 esbuild 项目上开启的一个问题,我收到了这个评论,使用 define,我们能够做到以下这些事情(如其文档所述):

这可以是在构建之间更改某些代码行为的一种方式,而无需更改代码本身。

有了这个新知识,我们可以添加类似于 define: { IIFE_ONLY: 'true' }(注意布尔值写成字符串)用于 iife,以及相反的('false')用于非 iife 构建(即 ESM)。就是这样。有了这个新的代码,我可以将所有文件都构建为 iife 以保留我们的传统方法,并且也可以构建为 ESM 以适应现代方法。所以总结一下,通过 define,我能够从我的构建中删除不必要的代码,而不必太多地更改代码,所有与格式类型相关的无用代码现在都被相应地删除了。

以下是提供在原始问题中的代码,需要稍微更新一下,如下所示,基本上将 window.Slick ? ... 更改为 IIFE_ONLY ? ...(这是新的 define 标志)。

// imports will be auto-dropped in iife by custom plugin
import { SlickEvent as SlickEvent_, Utils as Utils_ } from '../slick.core';

// for (iife) load `Slick` methods from global window object, or use imports for (cjs/esm)
const SlickEvent = IIFE_ONLY ? Slick.Event : SlickEvent_;
const Utils = IIFE_ONLY ? Slick.Utils : Utils_;

// ...

// then use it normally in the code...
const options = Utils.extend(true, {}, defaults, options);

对于 iife 构建将需要使用自定义插件(以删除所有 import)以及使用 define: { IIFE_ONLY: 'true' },这意味着三元运算符的第一个值将用于 iife,而对于 esm 构建,第二个值将被使用。

因此,所有这些后,iife 输出如下所示(删除了导入,并且我们使用全局窗口的 Slick 对象中的代码)。

"use strict";
(() => {
  // plugins/slick.cellcopymanager.js
  var SlickEvent = Slick.Event, Utils = Slick.Utils;
  function CellCopyManager() {
    // ...

而 ESM 的输出是一个单一的捆绑文件,其代码如下所示(它使用并保留了所有 import,并摒弃了全局的 Slick 对象)。

// plugins/slick.cellcopymanager.js
var SlickEvent5 = SlickEvent, Utils10 = Utils;
function CellCopyManager() {
  // ...

总之,iifeesm 构建现在只包括属于它们的构建格式类型的代码,没有多余的部分。这使得我的构建更加干净,比起最初使用的方法要小得多,这要归功于这种新方法,不再存在无法访问的死代码了(与最初相比,有很多行代码根本就无法访问且没有被删除)。

就这样!

现在我可以使用 esbuild 来处理所有构建格式类型,使我所有的用户都能满意,不管他们使用哪种格式。

英文:

To answer my own question after some trials and errors, I came up with the code below.

I first kept the same custom plugin that I had mentioned in the original question which removes all import when building as iife. The plugin is shown in the solution below (which is again the same code as previously mentioned in the question).

What is new in my answer, which was the missing piece, is to use esbuild#define and that suggestion came from an issue that I've opened on the esbuild project for which I receive this comment, with define we are able to do the following (as described in their docs)

> It can be a way to change the behavior some code between builds without changing the code itself

With that new knowledge, we can add something like define: { IIFE_ONLY: &#39;true&#39; } (notice the boolean is written as a string) for iife and the inverse (&#39;false&#39;) for non iife builds (i.e. ESM) and that's it. With this new code, I'm able to build all the files as iife to keep our legacy approach and also build as ESM for the modern approach. So in summary, with define I'm able to remove unnecessary code from my builds without changing the code too much, and all dead code (related to format type) are now removed accordingly

import { build } from &#39;esbuild&#39;;

const removeImportsPlugin= {
    name: &#39;remove-imports-plugin&#39;,
    setup(build) {
      build.onResolve({ filter: /.*/ }, (args) =&gt; {
        if (args.kind !== &#39;entry-point&#39;) {
          return { path: args.path + &#39;.js&#39;, namespace: &#39;import-ns&#39; }
        }
      });
      build.onLoad({ filter: /.*/, namespace: &#39;import-ns&#39; }, () =&gt; ({
        contents: `// empty string, do nothing`,
        loader: &#39;js&#39;,
      }));
    }
};

/** build as iife, every file will be bundled separately */
export async function buildIifeFile(file) {
  build({
    entryPoints: [file],
    format: &#39;iife&#39;,
    // add Slick to global only when filename `slick.core.js` is detected
    globalName: /slick.core.js/.test(file) ? &#39;Slick&#39; : undefined,
    define: { IIFE_ONLY: &#39;true&#39; },
    outfile: `dist/browser/${file.replace(/.[j|t]s/, &#39;&#39;)}.js`,
    plugins: [removeImportsPlugin],
  });
}

// bundle in ESM format into single file index.js
export function buildEsm() {
  build({
    entryPoints: [&#39;index.js&#39;],
    format: &#39;esm&#39;,
    target: &#39;es2020&#39;,
    treeShaking: true,
    define: { IIFE_ONLY: &#39;false&#39; },
    outdir: `dist/esm`,
  });
}

The code that was provided in the original question has to be updated a little bit and is shown below, basically changing this window.Slick ? ... to this IIFE_ONLY ? ... (which is the new define flag)

// imports will be auto-dropped in iife by custom plugin
import { SlickEvent as SlickEvent_, Utils as Utils_ } from &#39;../slick.core&#39;;

// for (iife) load `Slick` methods from global window object, or use imports for (cjs/esm)
const SlickEvent = IIFE_ONLY ? Slick.Event : SlickEvent_;
const Utils = IIFE_ONLY ? Slick.Utils : Utils_;

// ...

// then use it normally in the code...
const options = Utils.extend(true, {}, defaults, options);

The code that will be produced for an iife build will require to use both the custom plugin (to get rid of all import) and also use define: { IIFE_ONLY: &#39;true&#39; } which mean that the 1st value of the ternary operator will be used for iife and for esm build then the 2nd value will be used.

So after all of that, the iife output looks like below (imports are removed, and we use the code from the global window Slick object)

&quot;use strict&quot;;
(() =&gt; {
  // plugins/slick.cellcopymanager.js
  var SlickEvent = Slick.Event, Utils = Slick.Utils;
  function CellCopyManager() {
// ...

while the output for ESM is a single bundled file with the code shown below (it uses and keep all the import instead and gets rid of the global Slick object)

// plugins/slick.cellcopymanager.js
var SlickEvent5 = SlickEvent, Utils10 = Utils;
function CellCopyManager() {
// ...

in summary, both iife and esm builds are now only including code that belong to their build format type and nothing more. This allow my builds to be clean and a lot smaller in comparison to what I used at the beginning and that is thanks to this new approach, there's no more unreachable dead code anymore (as opposed to originally we had many lines that were simply unreachable and not removed).

and voilà!

I can now use esbuild for all my build format types and make all my users happy regardless of the format they use.

huangapple
  • 本文由 发表于 2023年6月8日 12:34:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/76428650.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定