Создание npm пакета на TypeScript с поддержкой CommonJS и ESM
CommonJS и ESM
При создании приложений на JavaScript есть выбор из двух систем модулей: CommonJS и ECMAScript Modules. Несмотря на недавний рост популярности ESM, CommonJS по-прежнему широко используется, не говоря уже, что в Node.js она используется по умолчанию. Чтобы убедиться, что ваш пакет npm может быть использован всеми, необходимо поддерживать обе системы модулей.
Подготовка проекта
CommonJS и ESM несовместимы друг с другом. Чтобы поддерживать оба варианта, пакет должен содержать две версии кода: одну для CommonJS и одну для ESM. Для поддержки использования в TypeScript потребуется включить определения типов. Поскольку форма экспортируемого API одинакова, можно использовать один и тот же TypeScript для CommonJS и ESM.
Вот глобальная структура кода, которая должна быть создана в пакете:
project
├── dist // сгенерированный пакет
│ ├── cjs
│ │ └── Код CJS
│ ├── esm
│ │ └── Код ESM
│ └── types
│ └── Определения типа TypeScript
└── src // исходный код
└── Исходный код на TypeScript
В следующих разделах описано, как настроить проект для получения такого результата.
Настройка TypeScript
TypeScript может одновременно создавать только один код. Чтобы создать код CommonJS и ESM, необходимы два разных конфигурационных файла TypeScript (tsconfig.json
). Хорошая новость заключается в том, что можно повторно использовать общие настройки и задать только отдельные части для CJS и ESM.
Начнём с создания базового конфигурационного файла TypeScript с общими настройками (tsconfig.base.json
):
{
"compilerOptions": {
"lib": [
"esnext"
],
"declaration": true,
"declarationDir": "./dist/types",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"baseUrl": ".",
"rootDir": "./src"
},
"include": [
"src"
],
"exclude": [
"dist",
"node_modules"
]
}
В этом файле, помимо прочего, указывается местоположение исходного кода, папки для исключения и каталог для определения типов.
Далее создадим два конфигурационных файла для CommonJS и ESM.
tsconfig.cjs.json
:
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "./dist/cjs",
"target": "ES2015"
}
}
tsconfig.esm.json
:
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "esnext",
"outDir": "./dist/esm",
"target": "esnext"
}
}
Конфигурация package.json
После настройки TypeScript перейдите к конфигурации package.json
. Здесь вам потребуется сделать несколько вещей. Нужно указать точки входа для пользователей CJS и ESM и определить скрипты для сборки пакета в обоих форматах.
Определение точек входа
Начнём с определения точек входа для пользователей CJS и ESM.
{
"name": "ts-cjs-esm",
"version": "1.0.0",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"files": [
"dist"
]
// ...
}
Есть основная точка входа для обращения к коду CJS и точка входа модуля для обращения к коду ESM. Оба формата используют одни и те же определения типов TypeScript. К сожалению, свойство main
отменяет свойство module
в Node.js, поэтому нужен дополнительный способ указать, как различные пользователи должны загружать библиотеку. Это можно сделать с помощью свойства exports
.
{
"name": "ts-cjs-esm",
"version": "1.0.0",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.mjs",
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"require": "./dist/cjs/index.js",
"import": "./dist/esm/index.mjs"
}
},
"files": [
"dist"
]
// ...
}
Используя свойство exports
, можно указать, как различные пользователи должны загружать библиотеку. Свойство require
указывает на код CJS, а свойство import
— на код ESM.
Расширение файлов .mjs
vs .js
Вы могли заметить, что ESM код имеет расширение .mjs
, в то время как CJS использует .js
. Это необходимо для того, чтобы отличать ESM код от CJS кода. Если пакет содержит только ESM код, в файле package.json
можно задать свойство type
в module
, чтобы указать, что пакет содержит ESM код. Это даст указание Node.js рассматривать все файлы как модули ES. К сожалению, в данном случае это невозможно, поскольку пакет содержит как CJS, так и ESM код. Это означает, что для обозначения ESM файлов необходимо использовать расширение .mjs
. Ещё одна вещь, усложняющая ситуацию, заключается в том, что TypeScript не позволяет указывать расширение выходного файла и всегда выдаёт файлы .js
. Чтобы обойти это, необходимо переименовывать выходные файлы и импортировать их после сборки.
Определение npm скриптов
После того как базовая настройка пакета выполнена, давайте продолжим с созданием нескольких скриптов для сборки пакета в обоих форматах.
{
"name": "ts-cjs-esm",
// ...
"scripts": {
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json && npm run rename:esm",
"build": "npm run build:cjs && npm run build:esm",
"clean": "rimraf dist",
"rename:esm": "/bin/zsh ./scripts/fix-mjs.sh",
"prepack": "npm run clean && npm run build"
}
// ...
}
Начнём с двух сценариев сборки, одного для CJS (build:cjs
) и одного для ESM (build:esm
), каждый из которых указывает на соответствующий файл конфигурации TypeScript. Как уже упоминалось, TypeScript не позволяет указывать расширение выходного файла, поэтому после сборки необходимо переименовать полученные файлы. Скрипт rename:esm
запускает сценарий shell, переименовывающий файлы .js
в .mjs
и обновляет ссылки на импорт. Для удобства также включаем сценарий clean
для очистки каталога результатов и build
— сборки пакета перед публикацией.
Установка зависимостей
В скриптах npm используются TypeScript и rimraf
. Убедитесь, что они установлены в качестве dev зависимостей:
npm install --save-dev typescript rimraf
Вспомогательный скрипт
Для поддержки сборки ESM кода понадобится вспомогательный скрипт, переименовывающий итоговые файлы и обновляющий ссылки на импорт. Создадим shell скрипт fix-mjs.sh
в папке scripts
:
for file in ./dist/esm/*.js; do
echo "Updating $file contents..."
sed -i '' "s/\.js'/\.mjs'/g" "$file"
echo "Renaming $file to ${file%.js}.mjs..."
mv "$file" "${file%.js}.mjs"
done
Этот сценарий перебирает все .js
файлы в папке dist/esm
. В каждом файле он заменяет .js
на .mjs
в содержимом файла, а затем переименовывает файл, чтобы у него было расширение .mjs
.
.gitignore
и .npmignore
Если собираетесь хранить исходники в git-репозитории, добавьте в проект файл .gitignore
, чтобы не включать в репозиторий ненужные файлы:
dist
node_modules
Поскольку выполняется сборка библиотеки, которая, скорее всего, будет распространяться, следует добавить файл .npmignore
, чтобы исключить ненужные файлы из npm пакета:
scripts
src
Пример исходного код
Для тестирования настройки создадим простые файлы исходного кода TypeScript в папке src
.
myModule.ts
:
export function myFunction() {
return 'Hello World!';
}
index.ts
:
export * from './myModule.js';
Обратите внимание, что при обращении к файлу myModule
используется расширение .js
. Это необходимо для корректной работы сборки ESM. При сборке ESM пакета вспомогательный скрипт обновляет расширение до .mjs
.
Сборка пакета
Чтобы убедиться в работоспособности установки, запустим сценарий сборки:
npm run build
Если всё в порядке, в папке dist
должен появиться соответствующий результат:
dist
├── cjs
│ ├── index.js
│ └── myModule.js
├── esm
│ ├── index.mjs
│ └── myModule.mjs
└── types
├── index.d.ts
└── myModule.d.ts
Подведение итогов
Создавая npm пакет, подумайте о поддержке CommonJS и ECMAScript Modules, чтобы вашим пакетом могли пользоваться все. Для поддержки CJS и ESM необходимо создать две версии кода, по одной для каждой модульной системы. Настроив проект определённым образом, можно создать пакет, поддерживающий обе системы модулей и включающий определения типов TypeScript. Таким образом, можно обеспечить бесперебойную работу для всех пользователей пакета, независимо от используемой ими системы модулей.