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

Если хотите глубже разобраться в JavaScript и TypeScript, научиться разрабатывать библиотеки, проектировать сложные приложения и уверенно работать с современным стеком, посмотрите программу курса «JavaScript‑разработчик. Продвинутый уровень».
Перед обучением можно пройти бесплатный вступительный тест. Он поможет оценить текущие знания JavaScript и понять, готовы ли вы к программе продвинутого уровня.
