Как собрать, покрыть тестами и опубликовать TypeScript-пакет в npm в 2022 году

от автора

В этой статье мы с вами создадим с нуля и опубликуем в  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   } }

Вот наиболее важные настройки:

  1. Наш основной файл будет находиться в папке src, поэтому "files": ["src/index.ts"].

  2. "target": "es2015", чтобы убедиться, что мы поддерживаем только современные платформы и не утяжеляем проект лишними оболочками.

  3. "module": "es2015". У нас будет стандартный ES-модуль (по умолчанию здесь CommonJS), чтобы у современных браузеров не было с ним никаких проблем; даже Node поддерживает его с 13-й версии.

  4. "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 правильные метаданные.

  1. Убедитесь, что для свойства main задан наш файл "main": "dist/index.js".

  2. Добавьте "types": "dist/index.d.ts" для наших пользователей TypeScript.

  3. Поскольку наша библиотека будет использоваться как ESM-модуль, нам также необходимо указать "type": "module".

  4. Также нужно задать имя (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 и другие инструменты упрощения работы со стилями. Регистрация открыта по ссылке для всех желающих.


ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/704398/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *