openlang

Default import из ESM, а типы как у CommonJS

Не получается нормально импортировать соседний пакет из ESM-части монорепозитория.

У @repo/ui в package.json нет поля "type", а exports устроен так. "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.cjs". У пакета-потребителя "type": "module", а moduleResolution выставлен в nodenext.

Ладно, пишу import ui from '@repo/ui' и набираю ui(. Редактор говорит, что это значение нельзя вызвать. Набираю ui., и автодополнение предлагает default. (На этом месте уже смешно.) Меняю на ui.default(), проверка типов проходит, а при запуске Node выбирает условие import и загружает настоящий ESM-файл, где такого свойства нет.

Перечитываю справочник по модулям в TypeScript Handbook. Там сказано, что .d.ts считается CommonJS, если в ближайшем package.json нет "type": "module". Выходит, типы описывают CJS-модуль, а во время выполнения загружается ESM. Default import CJS-модуля из ESM отдаёт весь module.exports, отсюда и лишний слой default.

Как это починить аккуратнее всего? Выбираю между отдельным файлом объявлений для каждого условия и простым переводом пакета на ESM.

Разговор продолжается 5

Следуя доводам
  1. Ссылка на ответ

    Лучше отдельный файл объявлений на каждое условие. Вложите types внутрь каждого условия, тогда формат определяется одним расширением, без оглядки на поле "type" пакета.

    "exports": {
      ".": {
        "import": {
          "types": "./dist/index.d.mts",
          "default": "./dist/index.mjs"
        },
        "require": {
          "types": "./dist/index.d.cts",
          "default": "./dist/index.cjs"
        }
      }
    }

    В справочнике по модулям в handbook пример устроен с той же вложенностью.

    Страшнее в нынешней схеме то, что ui.default() проходит проверку типов. Проверка типов знает только модуль, описанный в файле объявлений, а какой файл Node выберет при запуске, ей не видно. Подгоните код под неверные типы, и ошибка просто переедет во время выполнения.

    Перевести весь пакет на ESM тоже нормально. Но если кто-то ещё ходит через require, его типам нужна отдельная забота.

  2. Ссылка на ответ

    Способ Tess чинит сторону ESM. Сборка выпускает index.d.mts и index.d.cts, а exports разбит по условиям. На ui( редактор молчит, default из автодополнения пропал, а --traceResolution показывает, что условие import ведёт к index.d.mts.

    Но за другим условием надо следить. Если сборка просто копирует то же объявление в index.d.cts, вместе с export default, то старый скрипт .cts с import ui = require('@repo/ui') проходит проверку типов с ui.default(), а CJS-бандл кладёт функцию прямо в module.exports. Во время выполнения ui.default равен undefined. Исходная ошибка, только зеркально, на другом условии.

    Объявлению для CJS нужна своя форма.

    declare function ui(): string;
    export = ui;

    Так обе стороны сходятся. Правда, скрипт сборки теперь не копирует один файл объявлений, а пишет два разных, и это лишний шаг, с которым придётся жить.

  3. Ссылка на ответ

    Стоит проверить ещё две вещи.

    Первое - порядок условий. В документации Node сказано, что "types" всегда должен идти первым, а условия сопоставляются в том порядке, в каком записаны в объекте. Сейчас у вас это соблюдено, но после разбиения по условиям "types" должен стоять первым и внутри каждого блока.

    Во-вторых, проверьте, есть ли в tsconfig потребителя references. С project references модули из проекта по ссылке берутся из .d.ts, который этот проект собрал, а tsc --build сначала пересобирает устаревшие проекты в порядке зависимостей. Без ссылок компилятор может прочитать старый файл объявлений, оставшийся в dist, и никто этого не заметит.

    Пусть зависимости в package.json и references описывают один и тот же граф, тогда такие проблемы вылезают рано, и обычно в виде ошибки сборки, а не странной картинки в редакторе.

  4. Ссылка на ответ

    Пользуются только соседние пакеты? Уберите CJS-сборку, второй файл объявлений уйдёт вместе с ней. Останется найти тот единственный скрипт, который всё ещё зовёт require.

  5. Ссылка на ответ

    jo_ren, ваше замечание про references попало в точку. В tsconfig потребителя не было ни одной ссылки. Поэтому сразу после первой правки package.json редактор ещё какое-то время показывал старый index.d.ts, а мне долго казалось, что неверна сама правка.

    Порядок и так был правильный, так что не в нём дело. Ссылки добавлены, один прогон tsc --build, dist заполнился заново, и только тогда типы на экране совпали с правкой.

Другие стороны того же вопроса

  1. tryEval пропустил нарочно сломанный пакет

    Есть у меня небольшой обход оверлея, который оборачивает каждый пакет в builtins.tryEval и перечисляет упавшие. Он сообщил, ч…

    Ответы 4nix
  2. Ключи for_each - это адреса, а не ярлыки

    Ключ for_each - не ярлык. Это половина адреса ресурса, terraform_data.queue["prod-orders"], и состояние хранит объект именно…

    Ответы 6hcl
  3. Остановленное задание оболочки ещё не завершено

    Пришёл вопрос про правило супервизора, которое считает остановленный процесс завершённым, и маленький пример сразу показывает…

    Ответы 17bashpython