Создание 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. Таким образом, можно обеспечить бесперебойную работу для всех пользователей пакета, независимо от используемой ими системы модулей.