[RFC] Migrate webpack-encore to ESM
More and more npm packages are shipping as ESM. In node, the compatibility between ESM and CommonJS has some limitation:
- an ES module can import a CommonJS module
- an ES module can dynamically import a CommonJS module
- a CommonJS module cannot import an ES module (Node 22 has an experimental feature allowing it for synchronous object graphs, i.e. ES modules that don't use top-level await)
- a CommonJS module can dynamically import an ES module through
await import(...)(but this can only be used in an async context)
Shipping Encore as a CommonJS module means that we cannot upgrade our dependencies to versions that have migrated to ESM (as Encore.getConfig() is a synchronous API, any solution requiring an async context is a no-go).
Migrating to ESM needs to be done in a new major version as it is a BC break for downstream projects. This will require writing the webpack.config.js file in ESM, either by using type=module in the package.json or by using webpack.config.mjs instead (which is supported by webpack)
I'm 100% of favor of migrating Encore to ESM, it's something I've also thought recently.
Shipping Encore as a CommonJS module means that we cannot upgrade our dependencies to versions that have migrated to ESM (as
Encore.getConfig()is a synchronous API, any solution requiring an async context is a no-go).
Yeah, especially with #985 which forced us to introduce rpc-sync dependency to make ESLint API sync, and here in #1303 where I couldn't upgrade to an ESM version (and #1313 aswell).
Also, do we want to write JavaScript ESM compatible code, or do we want to add an additional build step like... TypeScript?
About TypeScript support mentionned in my previous message, of course it's a subject for another time, but in some recent versions of Node.js you can execute TypeScript code by stripping types at runtime, making it valid ECMAScript code. Of course, we should not use special TypeScript features like enum or decorators that are not ECMAScript features, but it should be fine.
Now that node has shipped support for require(ESM) in Node.js 20.19+ and 22.12+, with node.js 18 being EOL, we can ship ESM-only in the next major version of webpack-encore without drawback to users of Encore (by bumping our min Node version)
I forked this project time ago and tried rewriting it to TypeScript and then compiling it to support ESM and CJS, but there were too many files. Migrating is possible, but also too complex. In the end, I decided to switch to Vite. It already supports ESM and CJS, and it's faster.
Unfortunately, the existing community options require a Vite plugin and a Symfony package, in addition to the new configuration. However, I was thinking that it would be better to make a plugin that supports the the existing Encore bundle for a faster migration, and that's what I'm trying to do right now. I've already uploaded a prerelease version of this concept to npm, in case anyone wants to try it out.
It's a Vite plugin that generates the same entrypoints.json and manifest.json files as webpack-encore and works with the Encore bundle. It supports ESM and CJS, includes types, supports Stimulus UX bundles from the assets/controllers.json file, and also includes a custom Encore class, similar to the existing one. However, I recommend using the Vite configuration style, as the methods are a bit confusing when compared to Vite's options.
I think a new @symfony/vite-encore could be a better option that rewriting webpack-encore to support ESM.
@Eptagone this issue is purely about the implementation of Encore, and how the @symfony/webpack-encore package is published. It is not about providing a tool supporting to generate different things.
Supporting Vite in the Symfony ecosystem is a totally different topic (which is actually discussed, but won't be achieved by Encore itself)
Actually, Encore relies on synchronous dynamic requires for places dealing with optional dependencies (plugins registered conditionally for instance, like https://github.com/symfony/webpack-encore/blob/e63934470cb51dd66335f2ed4eb174e0374910bb/lib/plugins/vue.js#L28).
Using module.createRequire from node.js (available in node 12.2+ might be a solution to keep this implemented through a synchronous API