Неудачный опыт миграции Electron приложения на ECMAScript модули

Работая над своим стартовым шаблоном для Electron приложений я решил полностью отказаться от CommonJS модулей и использовать исключительно ECMAScript модули (далее ES модули или ESM).

Я очень хочу иметь единый стиль кода везде. В моём проекте, как и у многих, непосредственно исходный код написан с использованием ES модулей, а всё остальное (тесты, файлы конфигурации, дополнительные скрипты для сборки) написано с использованием CommonJS модулей. Меня это сильно напрягает и я хочу чтобы всё было в одном стиле -- ESM.

Кратко о модульных системах в NodeJS

Начиная с 13-й версии NodeJS поддерживает две системы модулей:

Важно знать:

Как NodeJS выбирает систему для конкретного файла

Тут всё просто. Существует два расширения для файлов: .cjs и .mjs которые определяют что это CommonJS или ES модуль соответственно.

Кроме этого, в package.json вы можете добавить свойство type со значением commonjs или module чтобы определить систему модулей по-умолчанию для файлов с расширением .js.

Проблемы Electron

С чего начинается любое Electron приложение? С файла main.js (или background.js) -- точки входа, которая отвечает за запуск, создание окон, проверку обновлений, работу с системными api и так далее. Если сильно упростить, то обычно этот файл имеет такое содержимое:

const { app, BrowserWindow } = require('electron')

app.whenReady().then(() =>
  new BrowserWindow().loadFile('index.html')
)

app.on('window-all-closed', () => app.quit())

Ничего особенного: подключение одной зависимости и вызов пары функций.

Я хочу использовать по всему проекту ESM, поэтому установил в package.json "type": "module" и переписал main.js:

- const { app, BrowserWindow } = require('electron')
+ import { app, BrowserWindow } from 'electron'

И моё ещё не написанное приложение уже падает с ошибкой:

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module

Это меня сильно удивило. Я использовал electron v12 у которого под капотом работает NodeJS 14.15. То есть ESM должны поддерживаться и работать.

Файлы проекта подключаются как CommonJS модули

Оказалось проблема в том, как electron, да и многие другие библиотеки, обрабатываются JavaScript файлы вашего проекта. А делают они это очень просто -- через вызов require:

То есть когда вы запускаете

electron /path/to/main.js

Где-то внутри, electron вызывает

require('/path/to/main.js')

Вызывающий файл electron это CommonJS модуль. А наш файл main.js -- ES модуль. CommonJS модули не могут подключать ES модули. Отсюда и ошибка.

И это главная проблема на пути к светлому ESM-будущему.

Обходные пути

Получается я не могу использовать ES модули из-за архитектуры Electron. Это большая проблема, но всё-таки решаемая.

К счастью, в моём проекте уже использовался сборщик. Так что я мог настроить его таким образом, чтобы преобразовывал ESM в CommonJS модули перед запуском Electron. Но чтобы nodeJS правильно определял какой это модуль конечные файлы нужно сохранять с расширением .cjs и это важно.

Проблемы со сборщиками

К моему большому удивлению, это оказалось сложнее чем я предполагал.

Некоторые инструменты не способны сохранять JavaScript в файлы с расширением не .js. Оно и логично, до недавних пор такой нужды не было.

Например используя tsc или esbuild я не могу изменить расширение итоговых файлов, в том случае если собираю не один, а сразу несколько. Все файлы будут сохранены с расширением .js.

Кроме этого многие сборщики/библиотеки думают про JavaScript файлы как про файлы исключительно с расширением .js. И когда приходит файл с расширением .cjs они не знают что с этим делать.

Например Vite, с которым я работаю, имеет возможность настроить шаблон выходных файлов [filename].[hash].cjs. Но до недавних пор он не понимал какой loader использовать для .cjs и падал с ошибкой. Пришлось отправить отдельный PR (который уже принят) чтобы он воспринимал .cjs файлы как JavaScript.

Проблемы с electron-builder

С исходным кодом я разобрался относительно небольшой ценой. Теперь можно писать ES модули, и Vite будет преобразовывать их в CommonJS модули перед запуском Electron.

Теперь, нужно обработанный код запаковать в исполняемое приложение. Для этого используется electron-builder. И тут меня ждали всё те же проблемы:

Это означает, что я никак не смогу написать конфиг electron-builderкак ES модуль.

Я решил пойти не компромисс и оставить конфиг electron-builder как CommonJS модуль с расширением .cjs. Это ставит крест на идее полностью отказаться от CommonJS модулей в проекте.

Но, это не помогло.

Дело в том, что read-config-file просто не воспринимает файлы .cjs как JavaScript:

if (config.endsWith('.js')) {
    require(config)
} else {
    // ... Это что угодно, но не JavaScript
}

Я отправил PR для исправления, но на момент публикации его так и не приняли.

Подчеркну electron-builderв принципе не способен обрабатывать JavaScript файл конфигурации в проектах где в package.json указано "type": "module".

Так что как временное решение пришлось переписать конфигурацию с .js на .json, отказавшись от некоторых возможностей.

Проблемы с экосистемой в целом

И подобный подход встречается в очень многих библиотеках, что вызывает массу проблем, если вы хотите писать ES модулями.

Больше проблем чем пользы

В какой-то момент я осознал, что желаемого мною результата не добиться. Я то и дело сталкиваюсь с проблемами, невозможностью использовать тот или иной пакет, вынужден идти на компромиссы и отказываться от каких-то возможностей. И это всё только на этапе подключения файлов. А ведь различий в модульных системах намного больше чем название одной функции. Так что мне пришлось отказаться от этой идеи на ближайшие несколько лет.

@Kozack
26.02.2021 21:32 UTC
Первоисточник

Комментарии

@vanyapodgornov
26.02.2021 16:55 UTC
0

А что если после export something, дописать module.exports = something? Может тогда require сможет его подключить?

@Kozack
26.02.2021 16:56 UTC
0

Вы не можете использовать в одном файле две системы модулей.

@JustDont
26.02.2021 18:23 UTC
+1
Вы не можете использовать в одном файле и require и import. Либо то, либо другое.

Это не так. Вы вполне можете оставить require в ESM, только вам его придётся сначала получить. Тем не менее, при необходимости, имеющийся код с require можно не менять.


Но чтобы nodeJS правильно определял какой это модуль конечные файлы нужно сохранять с расширением .cjs и это важно.

Да ну нет же, достаточно передать параметр типа модулей по умолчанию (или прописать в package.json). Вообще пытаться жить с .cjs и .mjs вместо хотя бы .js и .mjs (а еще лучше вообще взять typescript, и будет .js и .ts, где с ресолвом импортов будет разбираться tsc, делающий это куда лучше ноды) — это из серии "мы легких путей не ищем". И неудивительно, что вы тут же нажили этим дополнительных проблем.


А в целом да, сразу было ясно, что ограничение "из CommonJS ресолвим только CommonJS" приведет к тому, что светлой эры повального ESM не наступит, пока не подтянутся инструменты.

@justboris
27.02.2021 17:13 UTC
0

В принципе, вся суть статьи раскрывается вот в этом предложении:


Вызывающий файл electron это CommonJS модуль.

Пока electron это не исправит на своей стороне (вот issue на эту тему) о нативных модулях в electron можно только мечтать.


А всех трудностей со сборщиками можно было избежать, просто удалив type: "module" из package.json, разве нет?

@Kozack
27.02.2021 20:23 UTC
0
А всех трудностей со сборщиками можно было избежать, просто удалив type: "module" из package.json, разве нет?

Так то да, но вся суть же ж была именно в том, чтобы запустить проект с ES модулями как основными. А для этого type: "module" и нужен.

@i360u
28.02.2021 08:40 UTC
+1

В свое время решал эту проблему просто подключая в Electron уже готовые сборки из ESM. Это может и не очень красиво, но работало...

@megahertz
01.03.2021 04:34 UTC
0

Получается два варианта


  • Смириться
  • Взять (или написать) обертку по типу electron-compile, которая будет выкинута как только инструменты созреют
@dikium
24.09.2021 12:44 UTC
+1

Интересно почему нельзя было разрешить commonjs модули в ESM? Если бы так было сделано в node.js то не надо было бы всех этих танцев и новых расширений. :/