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: 'Slick'
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 <script>
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 './slick.core.js';
import { Draggable as SlickDraggable_, MouseWheel as SlickMouseWheel_, Resizable as SlickResizable_ } from './slick.interactions.js';
// 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 '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: `// empty string, do nothing`,
loader: 'js',
};
})
}
};
build({
entryPoints: ['slick.grid.js'],
color: true,
bundle: true,
minify: false,
target: 'es2015',
sourcemap: false,
logLevel: 'error',
format: 'iife',
// globalName: 'Slick', // only for the core file
outfile: 'dist/iife/slick.grid.js',
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
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 asiife
. So the plugin is the same code and is shown in the question above.What is new in my answer, which was the missing piece, is to use
esbuild#define
which was provided as a suggestion from an issue that I've opened on the esbuild project and for which I have received this comment, withdefine
we are able to do the following (as described in their docs)With that new knowledge, we can add something like
define: { IIFE_ONLY: 'true' }
(notice the boolean is written as a string) foriife
and the inverse ('false'
) for non iife builds (i.e. ESM) and that's it. With that new code in place, I'm able to build all the files asiife
(to keep our legacy approach) and also build as ESM for the modern approach. So in summary, withdefine
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 accordinglyThe 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 thisIIFE_ONLY ? ...
(which is the newdefine
flag)The code that will be produced for an
iife
build will require to use both the custom plugin (to get rid of allimport
) and also usedefine: { IIFE_ONLY: 'true' }
which mean that the 1st value of the ternary operator will be used foriife
and foresm
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 windowSlick
object)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 globalSlick
object)in summary, both
iife
andesm
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 with roughly the same build size as before.