В этой статье мы с вами создадим с нуля и опубликуем в NPM TypeScript-пакет, не забыв про Jest для покрытия тестами.
Мы инициализируем проект, настроим TypeScript, напишем для него тесты в Jest и опубликуем его в NPM.
Наш проект
Наша простая библиотека будет называтся digx. Она позволяет “выкапывать” значения из вложенных объектов по заданному пути (аналогично lodash get).
Например:
const source = { my: { nested: [1, 2, 3] } }
digx(source, "my.nested[1]") //=> 2
Для раскрытия темы этой статьи это не так важно что она делает, главное чтобы она была достаточно проста и тестируема.
Модуль npm можно найти здесь. GitHub-репозиторий находится здесь.
Инициализируем проект
Давайте начнем с создания пустого каталога и его инициализации.
mkdir digx
cd digx
npm init --yes
Команда npm init --yes
создаст файл package.json
и заполнит его некоторыми дефолтными значениями (которые вы, возможно, захотите изменить).
И давайте сразу настроим git-репозиторий в этой же папке.
git init
echo "node_modules" >> .gitignore
echo "dist" >> .gitignore
git add .
git commit -m "initial"
Сборка библиотеки
Мы будем использовать TypeScript. Давайте установим его.
npm i -D typescript
Далее мы создадим tsconfig.json
со следующей конфигурацией:
{
"files": ["src/index.ts"],
"compilerOptions": {
"target": "es2015",
"module": "es2015",
"declaration": true,
"outDir": "./dist",
"noEmit": false,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
}
}
Вот наиболее важные настройки:
Наш основной файл будет находиться в папке
src
, поэтому"files": ["src/index.ts"]
."target": "es2015"
, чтобы убедиться, что мы поддерживаем только современные платформы и не утяжеляем проект лишними оболочками."module": "es2015"
. У нас будет стандартный ES-модуль (по умолчанию здесь CommonJS), чтобы у современных браузеров не было с ним никаких проблем; даже Node поддерживает его с 13-й версии."declaration": true
- потому что нам нужны файлы декларацийd.ts
. Нашим пользователям TypeScript они точно потребуются.
Большинство остальных пунктов — это просто различные необязательные проверки TypeScript, которые лично я предпочитаю включать.
Откройте package.json
и обновите раздел "scripts":
"scripts": {
"build": "tsc"
}
Теперь мы можем запустить сборку с помощью npm run build
... Что выдаст ошибку, ведь у нас еще нет кода, который бы мы могли собирать.
Но мы начнем с другого конца.
Добавление тестов
Как ответственные взрослые, коими мы являемся, мы начнем с тестов. Мы будем использовать jest
, потому что он прост и прекрасен.
npm i -D jest @types/jest ts-jest
Для того, чтобы Jest понимал TypeScript, нам потребуется пакет ts-jest
. В качестве альтернативы мы можем использовать babel
, но он потребует дополнительной настройки и дополнительных модулей. Не усложнять себе работу — в наших же интересах.
Инициализируйте файл конфигурации jest с помощью
./node_modules/.bin/jest --init
Дальше просто прожмите Enter для каждого вопроса. Сейчас нас вполне устроят настройки по умолчанию.
Это команда создаст файл jest.config.js
с некоторыми значениями по умолчанию и добавит скрипт "test": "jest"
в package.json
.
Откройте jest.config.js
, найдите строку, начинающуюся с preset
, и измените ее следующим образом:
{
// ...
preset: "ts-jest",
// ...
}
Наконец, создайте каталог src
и наш тестовый файл src/digx.test.ts
и внесите туда следующее:
import dg from "./index";
test("works with a shallow object", () => {
expect(dg({ param: 1 }, "param")).toBe(1);
});
test("works with a shallow array", () => {
expect(dg([1, 2, 3], "[2]")).toBe(3);
});
test("works with a shallow array when shouldThrow is true", () => {
expect(dg([1, 2, 3], "[2]", true)).toBe(3);
});
test("works with a nested object", () => {
const source = { param: [{}, { test: "A" }] };
expect(dg(source, "param[1].test")).toBe("A");
});
test("returns undefined when source is null", () => {
expect(dg(null, "param[1].test")).toBeUndefined();
});
test("returns undefined when path is wrong", () => {
expect(dg({ param: [] }, "param[1].test")).toBeUndefined();
});
test("throws an exception when path is wrong and shouldThrow is true", () => {
expect(() => dg({ param: [] }, "param[1].test", true)).toThrow();
});
test("works tranparently with Sets and Maps", () => {
const source = new Map([
["param", new Set()],
["innerSet", new Set([new Map(), new Map([["innerKey", "value"]])])],
]);
expect(dg(source, "innerSet[1].innerKey")).toBe("value");
});
Эти модульные тесты дают хорошее представление о том, что мы создаем.
Наш модуль экспортирует одну функцию, digx
. Она принимает любой объект, строковый параметр path
и опциональный параметр shouldThrow
, который вызывает исключение, если вложенная структура исходного объекта не содержит указанный путь.
В качестве вложенных структур могут выступать объекты, массивы, Map’ы и Set’ы.
Запустить тесты можно с помощью команды npm t
; Конечно, сейчас они будут выдавать ошибку — так и должно быть.
Теперь откройте файл src/index.ts и
скопируйте туда это:
export default dig;
/**
* Функция dig, которая принимает любой объект с вложенной структурой и путь для него,
и возвращает значение, которое было найдено по этому пути или undefined, если значение
не найдено
*
* @param {any} Объекты с вложенной структурой.
* @param {string} path - Строка с путем, например, `my[1].test.field`
* @param {boolean} [shouldThrow=false] - Опционально пробрасывает исключение, если ничего не найдено
*
*/
function dig(source: any, path: string, shouldThrow: boolean = false) {
if (source === null || source === undefined) {
return undefined;
}
// split path: "param[3].test" => ["param", 3, "test"]
const parts = splitPath(path);
return parts.reduce((acc, el) => {
if (acc === undefined) {
if (shouldThrow) {
throw new Error(`Could not dig the value using path: ${path}`);
} else {
return undefined;
}
}
if (isNum(el)) {
// Для массива
const arrIndex = parseInt(el);
if (acc instanceof Set) {
return Array.from(acc)[arrIndex];
} else {
return acc[arrIndex];
}
} else {
// Для объекта
if (acc instanceof Map) {
return acc.get(el);
} else {
return acc[el];
}
}
}, source);
}
const ALL_DIGITS_REGEX = /^\d+$/;
function isNum(str: string) {
return str.match(ALL_DIGITS_REGEX);
}
const PATH_SPLIT_REGEX = /\.|\]|\[/;
function splitPath(str: string) {
return (
str
.split(PATH_SPLIT_REGEX)
// Удаляем пустые строки
.filter((x) => !!x)
);
}
Если честно, то реализация могла бы быть и получше, но для нас важнее то, что тесты проходятся уже сейчас. Попробуйте сами, запустив npm t
.
Теперь, если мы запустим npm run build
, мы должны увидеть каталог dist с двумя файлами, index.js и index.d.ts.
Теперь мы готовы к публикации.
Публикация npm-пакета
Зарегистрируйтесь на npm, если вы еще этого не сделали.
Затем залогиньтесь через свой терминал с помощью npm login
.
Мы всего в одном шаге от публикации нашего замечательного нового пакета. Тем не менее, есть несколько вещей, о которых нам еще нужно позаботиться.
Во-первых, давайте удостоверимся, что в нашем package.json
правильные метаданные.
Убедитесь, что для свойства main задан наш файл
"main": "dist/index.js"
.Добавьте
"types": "dist/index.d.ts"
для наших пользователей TypeScript.Поскольку наша библиотека будет использоваться как ESM-модуль, нам также необходимо указать
"type": "module"
.Также нужно задать имя (name) и описание (description).
Далее нам нужно позаботиться о файлах, которые мы хотим опубликовать. Мы бы не хотели опубликовать какие-либо файлы конфигурации или исходные, или тестовые файлы.
Для этого мы могли бы воспользоваться .npmignore
, где мы перечислим все файлы, которые мы НЕ ХОТИМ публиковать. Но вместо этого я бы предпочел иметь “вайтлист”, поэтому давайте воспользуемся полем files
в package.json
, чтобы указать файлы, которые мы ХОТИМ включить.
{
// ...
"files": ["dist", "LICENSE", "README.md", "package.json"],
// ...
}
Наконец, мы готовы опубликовать наш пакет.
Запустите
npm publish --dry-run
И убедитесь, что включены только необходимые файлы.
Когда мы будем готовы, мы, наконец, сможем запустить
npm publish
Финальная проверка
Давайте создадим новый проект и установим наш модуль.
npm install --save digx
А теперь давайте напишем простую программу для проверки.
import dg from "digx"
console.log(dg({ test: [1, 2, 3] }, "test[0]"))
Это наши типы, замечательно!
DIGX DIGX типы доступны прямо из коробки.
Теперь запустите его с помощю node index.js,
и вы должны увидеть на экране 1.
Заключение
Мы создали с нуля простой npm-пакет и успешно опубликовали его.
Наша библиотека предоставляет ESM-модуль, типы для TypeScript и покрыта тестами с использованием jest
.
Никто не будет отрицать, что это было не так уж и сложно.
Скоро состоится открытое занятие «CSS-in-JS. Удобный способ управлять стилями». На уроке обсудим Styled components, Linaria, Astroturf и другие инструменты упрощения работы со стилями. Регистрация открыта по ссылке для всех желающих.
Комментарии (3)
return
09.12.2022 11:27+2В наш 2022 сделать и опубликовать пакет не так просто, потому что нужно еще и про es-модули побеспокоиться
megahertz
09.12.2022 15:41"files": ["dist", "LICENSE", "README.md", "package.json"]
Можно оставить только dist, остальные 3 файла стандартные и добавляются npm принудительно.
Для полноты картины не хватает линтинга и Github Actions Workflow
return
Если брать в расчет поддержку самой старой поддерживаемой версии ноды (14), то можно использовать es2020