Настраиваем удобный npm проект для себя и команды или немного о современных фронтенд инструментах


    Всем привет. Недавно мне попалась задача настроить оборот приватных npm пакетов. Все звучало очень интересно и многообещающе пока не оказалось, что делать там совсем не много. Тут бы все и закончилось, но возникла вторая задача — написать демо репозиторий для npm пакета, который можно было бы взять, клонировать и на его базе быстро создать что-то полезное и в едином стиле.


    В результате получился проект с настроенным форматированием, кодстайлом, тестами на каждый пулл, лимитами на покрытие кода, отчетом о покрытии кода и автоматической документацией. Плюс удобная публикация в npm. Подробности о настройке — под катом.


    Требования


    Cначала я прикинул что у нас уже есть:


    • Новые проекты пишутся на TypeScript
    • Кроме новых проектов, есть еще куча проектов на чистом JavaScript
    • Есть требования писать тесты и результаты нужно отправлять на анализ

    Потом прикинул свои хотелки — раз уж есть время и желание, почему бы не разгуляться. Что я хочу еще:


    • Хочу единый стиль форматирования
    • Хочу единый стиль TypeScript
    • Хочу документацию, но не хочу ее писать
    • Вообще хочу все автоматизировать по максимуму. Что бы фыр-фыр-фыр и в продакшн

    В итоге требования оформились в следующее:


    • Модуль должен быть на TypeScript и проверен с помощью TsLint
    • Модуль должен быть используемым из-под TypeScript и из-под JavaScript
    • Тесты должны быть настроены на git hook, минимальное покрытие кода должно быть тоже настроено, статистика должна быть
    • Форматирование должно быть настроено
    • Документация должна создаваться из кода
    • Публикация должна быть удобной и единообразной
    • Все что можно должно быть автоматизировано

    Вроде бы неплохо, нужно пробовать.


    Предварительные телодвижения


    Создаем (клонируем) репозиторий, инициализируем package.json, ставим локально TypeScript. Вообще все зависимости ставим локально, т.к. все будет уходить на сервер. Не забываем фиксировать зависимости версий.


    git init
    npm init
    npm i -D typescript
    ./node_modules/.bin/tsc --init

    Тут же на месте нужно подправить tsconfig.json под себя — выставить target, libs, include/exclude, outDir и остальные опции.


    Стиль форматирования


    Для сохранения единообразия форматирования я взял два инструмента. Первый это файл .editorconfig. Он понимается всеми основными IDE (WebStorm, VSCode, Visual Studio и т.д.), не требует установки ничего лишнего и работает с большим количеством типов файлов — js, ts, md, и так далее.


    #root = true
    
    [*]
    indent_style = space
    end_of_line = lf
    charset = utf-8
    trim_trailing_whitespace = true
    insert_final_newline = true
    max_line_length = 100
    indent_size = 4
    
    [*.md]
    trim_trailing_whitespace = false

    Теперь автоформатирование будет вести себя более-менее одинаково у меня и у коллег.


    Второй инструмент — prettier. Это npm пакет, который проверяет, и по возможности, автоматически корректирует ваше форматирование текста. Установите его локально и добавьте первую команду в package.json


    npm i -D prettier

    package.json


    "prettier": "prettier --config .prettierrc.json --write src/**/*.ts"

    У prettier нет команды init, так что конфигурировать его нужно вручную. Создайте .prettierrc.json в корне проекта вот примерно с таким спорным содрежанием (если что — пост совсем не о том, какие кавычки лучше, но вы можете попробовать)


    .prettierrc.json


    {
        "tabWidth": 4,
        "useTabs": false,
        "semi": true,
        "singleQuote": true,
        "trailingComma": "es5",
        "arrowParens": "always"
    }

    Теперь создайте папку src, в ней создайте index.ts с каким-то условным содержанием и попробуйте запустить prettier. Если ему не понравится ваше форматирование — он его исправит автоматически. Очень удобно. Если вы не хотите вспоминать об этом только во время коммита/пуша можно настроить его на автозапуск или поставить расширение для студии.


    Стиль кода


    Со стилем кода все тоже не сложно. Для JavaScript есть eslint, для TypeScript есть tslint. Ставим tslint и создаем tsconfig.js для хранения настроек


    npm i -D tslint
    ./node_modules/.bin/tslint --init

    package.json


    "lint": "tslint -c tslint.json 'src/**/*.ts' 'tests/**/*.spec.ts'"

    Вы можете написать собственные правила, а можете использовать уже существующие правила с помощью параметра extend. Вот, например, конфиг от airbnb.


    Разработчики tslint шутят
    module.exports = {
        extends: "./tslint-base.json",
        rules: {
            "no-excessive-commenting": [true, {maxComments: Math.random() * 10}]
        }
    };

    Ну разве это не красиво?


    Есть важный момент — tslint и prettier пересекаются по функционалу (например в длинне строки или "висящих" запятых). Так что, если вы будете использовать оба — нужно будет следить за соответствием или отказаться от чего-то.


    И еще, для тех кто хочет проверять не все файлы, а только staged — есть пакет lint-staged


    Тесты


    Что нам нужно от тестов прежде всего? Во-первых, чтобы они запускались автоматически, во-вторых контроль покрытия, в-третьих какой-то отчет, желательно в lcov формате (если что — lcov отлично понимается разными анализаторам — от SonarQube до CodeCov). Автоматизацией мы займемся чуток позже, пока настроим сами тесты.


    Ставим karma, jasmine и весь соответствующий ей обвес


    npm i -D karma karma-jasmine jasmine karma-typescript karma-chrome-launcher @types/jasmine
    ./node_modules/.bin/karma init

    Немного модифицируем karma.conf.js и сразу настроим работу с покрытием


    karma.conf.js
    karmaTypescriptConfig : {
        include: ["./src/**/*.ts", "./tests/**/*.spec.ts"],
        tsconfig: "./tsconfig.json",
    
        reports: {
            "html": "coverage",
            "lcovonly": {
                directory: './coverage',
                filename: '../lcov.dat'
            }
        },
        coverageOptions: {
            threshold: {
                global: {
                    statements: 60,
                    branches: 60,
                    functions: 60,
                    lines: 60,
                    excludes: []
                },
                file: {
                    statements: 60,
                    branches: 60,
                    functions: 60,
                    lines: 60,
                    excludes: [],
                    overrides: {}
                }
            }
        },
    }

    И, конечно, не забываем дописать новую команду в package.json


    package.json


    "test": "karma start"

    Если вы используете или планируете использовать CI то лучше поставить HeadlessChrome:


    npm i -D puppeteer

    И доконфигурировать Karma (Chrome исправить на ChromeHeadless) плюс еще кое-что. Правки можно посмотреть в демо репозитории.


    Запустите тесты, проверьте что все работает. Заодно проверьте отчет о покрытии (он находится в папке coverage) и уберите его из-под контроля версий, в репозитории он не нужен.


    Отчет:



    Стиль коммитов


    Коммиты тоже можно унифицировать (и заодно довести кого-то до белого каления, если переборщить, так что лучше без фанатизма). Для этого я взял commitizen. Он работает в форме диалога, поддерживает conventional-changelog формат (из его коммитов можно создавать changelog) и для него есть VsCode плагин


    npm i -D commitizen
    npm i -D cz-conventional-changelog

    cz-conventional-changelog это адаптер, который отвечает за вопросы, и как следствие, за формат ваших коммитов


    Добавьте новую команду в scripts секцию


    "commit":"git-cz"

    И новую секцию package.json — config для commitizen


    "config": {
            "commitizen": {
                "path": "./node_modules/cz-conventional-changelog"
            }
        }

    Диалог с commitizen выглядит так:



    Документация


    Теперь к документации. Документация у нас будет двух видов — из кода и из коммитов. Для документации из кода я взял typedoc (аналог esdoc но для TypeScript). Он очень просто ставится и работает. Главное не забудьте убрать результаты его трудов из-под контроля версий.


    npm i typedoc -D

    и обновляем package.json


    package.json


    "doc": "typedoc --out docs/src/ --readme ./README.md"

    Флаг --readme заставит включить readme в главную страницу документации что удобно.


    Второй тип документации это changelog, и с ним нам поможет conventional-changelog-cli пакет.


    npm i -D conventional-changelog-cli

    package.json


    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"

    От angular здесь только форматирования и ничего больше. Теперь для того что бы обновить changelog достаточно запусть npm run changelog. Главное внимательно писать коммиты. Ну мы же всегда пишем идеальные коммиты, так что это проблемой быть не должно.


    Билд


    Поскольку наш пакет должен работать и для JavaScript, нам нужно превратить TypeScript в JavaScript. Кроме того, неплохо было бы сделать еще и минифицированный бандл, просто на всякий случай. Для этого нам понадобится uglifyjs и немного подправить package.json


    npm i -D uglifyjs

    package.json


    "clean":"rmdir dist /S /Q",
    "build": "tsc --p ./ --sourceMap false",
    "bundle": "uglifyjs ./dist/*.js --compress --mangle --output ./dist/index.min.js"

    Кстати, если вы хотите контролировать размер вашего проекта, есть еще два интересных пакета



    Их точно также можно интегрировать в процесс коммита/пуша/паблиша что бы держаться в допустимых размерах бандла. Очень, очень полезно.


    Автоматизация


    Ну вот, основные шаги мы уже сделали, теперь все нужно автоматизировать иначе работать будет откровенно неудобно.


    Для этого нам понадобится еще один пакет — husky. Он переписывает git hooks и вызывает сопоставленные команды из package.json. Например, когда вы делаете коммит, сработает precommit, push — prepush и так далее. Если скрипт вернет ошибку, коммит упадет.


    npm i -D husky

    package.json


    "precommit":"npm run prettier",
    "prepush": "call npm run lint && call npm run test"

    Тут есть важный нюанс, использование call синтаксиса не кроссплатформенно и на unix системах не взлетит. Так что если хотите все сделать по-честному придется поставить еще и npm-run-all пакет, он делает все тоже самое но кросплатформенно.


    Публикация


    Ну вот мы и дошли до публикации нашего (пусть и пустого) пакета. Давайте подумаем, что мы хотим от публикации?


    • Еще раз все протестировать
    • Собрать билд артефакты
    • Собрать документацию
    • Поднять версию
    • Запушить теги
    • Отправить в npm

    Руками это все делать — грустно. Или забудешь что-то, или чеклист писать нужно. Нужно тоже автоматизировать. Можно поставить еще один пакет — unleash. А можно воспользоваться нативными хуками самого npm — preversion, version, postversion, например вот так:


    "preversion": "npm run test",
    "version": "call npm run clean && call npm run build && npm run bundle && call npm run doc && call npm run changelog && git add . && git commit -m 'changelogupdate' --no-verify",
    "postversion": "git add . && git push && git push --tags",

    Осталось указать для package.json что включать в пакет, точку входа и путь к нашим типам (не забудьте указать --declaration флаг в tsconfig.json что бы получить d.ts файлы)


    package.json


    "main": "./dist/index.min.js",
    "types": "./dist/index.d.ts",
    "files": [
            "dist/",
            "src/",
            "tests/"
        ]

    Ну вот вроде бы и все. Теперь достаточно выполнить


    npm version ...
    npm publish

    И все остальное будет сделано в автоматическом режиме.


    Бонус


    В качестве бонуса есть демо репозиторий, который все это поддерживает + CI с помощью travis-ci. Напомню, там настроен HeadlessChrome так что, если вам это важно, советую туда заглянуть.


    Благодарности


    Большая благодарность Алексею Волкову за его доклад на JsFest, который и стал основой этой статьи.


    Спасибо max7z, indestructible, justboris за уточнение, что пути к локальным зависимостям можно не указывать.


    И немного статистики


    • Размер node_modules: 444 MB (NTFS)
    • Количество зависимостей первого уровня: 17
    • Общее количество использованых пакетов: 643

    Полезные ссылки


    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 44

      +1

      Спасибо, прекрасная подборка инструментов.
      Два момента хотелось бы уточнить:


      • Есть ли визуализация в отчете по покрытию тестами? Имеется в виду красиво показать какие части исходного кода покрыты а какие не покрыты. Как то я привык настраивать instanbul и remap-istanbul для этого.
      • У меня приблизительно такие же конфиги и команда call не используется. Работает на windows и ubuntu:
        "coverage": "npm run coverage-remap && npm run coverage-report-html && npm run coverage-report-clover",
        0

        Да визуализация тоже есть, вот фото:



        За call — отдельное большое спасибо. Если не возражаете добавлю в статью со ссылкой.

        0

        del

          0
          ./node_modules/.bin/tsc --p ./ --sourceMap false


          А есть ли надобность писать путь к компилятору? Если упростить до `tsc --p ./ --sourceMap false`, это же одно и тоже будет? Я думал, что всегда приоритет дается для ./node_modules/.bin, а потом уже смотрятся глобальные пакеты.

          Или тут есть подводные камни?
            0

            Для Windows — надо точно. Иначе или не найдет, или возьмет глобальный. То, что вы описали подходит когда мы импотрируем зависимость, например через require. Но это тоже не совсем точно. Вот доки

              +2

              По-моему, если запускать через скрипт npm, то полный путь можно не указывать.

                0
                Только что проверил — действительно, можно не указывать. Надо еще проверить что будет запускаться сначала — глобальные или локальные модули и как это работает на Unix. А так, да замечательно, спасибо.
                  +1

                  Документация npm говорит, что путь node_modules/.bin добавляется в PATH. Раз фича задокументирована — должна работать на всех поддерживаемых платформах (в т.ч. Windows)


                  Оффтоп

                  Довольно часто вижу этот анитипаттерн с явным указыванием node_modules/.bin. Документацию к рабочим инструментам никто не читает, я так понимаю?

                    0
                    Обновил статью, спасибо всем сказал :)
                      0
                      Неоднократно наблюдал как шелу сносит крышу относительным путем в PATH, так что без предварительного «hash -r» может случиться не то что вы ожидаете. Я думаю из-за этого нет доверия этой фиче.
              0
              возможно я не очень внимательно читал, но все описанное на мой взгляд проще реализовать через gitlab ci (или любой другой ci) + nexus
              Описываем stages с тестами, билдами и публикацией в nexus
                0
                Да, любой CI подойдет. Да и он тут тоже есть ввиде Travis. Но для меня есть два важных момента
                * Все проблемы с кодом видны сразу и без коммитов. Не нужно ждать пока пойдет пулл на сервер и он отработает.
                * Все самодостаточно и CI можно вообще не поднимать.

                С другой стороны, за это приходится платить большим объемом реально не нужных зависимостей.

                Так что как мне сегодня сказали — нужно подходить в каждом случае индивидуально.
                  +1
                  Все самодостаточно и CI можно вообще не поднимать.

                  CI нужен, чтобы избавиться от проблем "а у меня локально все работает". Может быть, у вас локально тесты пройдут, а у коллеги чего-то не сработает. CI-билд позволяет проверить, что все зависимости указаны, и проект сможет сбилдиться с нуля. Гит-хуки могут быть лишь дополнением к этой системе, но никак не заменой.

                    0
                    Согласен
                0
                Отличная сборка, вот только жаль, что все борятся за размер билда, а до размера окружения разработчика похоже никому уже нет дела.
                450Мб одного только бойлерплейта
                image
                  0
                  А что не так? Винты нынче не дорогие, ставить надо один раз, как раз кофе приготовить, вроде все ок, нет?
                    +1
                    Кстати, typedoc версии 0.11.1 тащит за собой старую версию тайпскрипта (2.7.2). Если выбросить ее и puppeteer, то самыми тяжелыми останутся основной ts (~36Mb) и uws (~27Mb) а общий размер сократится до 150Mb.
                    0

                    Ожидал под спойлером увидеть разбор, откуда берется 450 Мб и как их можно уменьшить, а там уже всем надоевшая стандартная картинка.

                      0

                      Самым прожорливым оказался как и ожидалось prettier, который тащит за собой хром.
                      И объем у меня вышел почти на 100Мб больше, чем у автора видимо из-за него же.


                      Total, Mb    | Impact, Mb   | Package
                      ------------ | ------------ | ------------
                              36.0 |         36.0 | typescript
                              41.4 |          5.4 | tslint
                              49.2 |          7.8 | prettier
                              73.8 |         24.6 | eslint
                             148.0 |         74.2 | karma karma-jasmine jasmine ...
                             439.0 |        291.0 | puppeteer
                             452.0 |         13.0 | commitizen cz-conventional-changelog
                             493.0 |         41.0 | typedoc
                             496.0 |          3.0 | conventional-changelog-cli
                             496.0 |          0.0 | uglifyjs
                             528.0 |         32.0 | bundlesize
                             542.0 |         14.0 | size-limit
                             542.0 |          0.0 | husky
                        0
                        Самым прожорливым оказался как и ожидалось prettier, который тащит за собой хром.

                        Как это у вас вышло? В пустой папке ставлю prettier, получаю


                        $ npm i prettier
                        
                        + prettier@1.13.7
                        added 1 package from 1 contributor and audited 1 package in 1.982s
                        found 0 vulnerabilities
                        
                        $ ls node_modules
                        prettier
                        
                        $ du -h node_modules
                          0B    node_modules/.bin
                        7.9M    node_modules/prettier
                        7.9M    node_modules

                        8 Мб, никакого хрома.


                        И что значат колонки в таблице, чем Total отличается от Impact?

                          0

                          Прошу прощения, опечатался! Имелся в виду puppeteer (и это видно из таблицы).
                          Total — рост общего размера директории node_modules, Impact — влияние пакета на этот рост.

                          0
                          У меня bundlesize+sizelimit не стояли на момент померять, но стоят puppeteer, вот где то и вышло 440 МБ. Да, по сути он самый прожорливый. Если его выбросить, взять аналогичный обвес но для JS + chai + mocha, можно я думаю, влезть в 150-200 МБ. Надо будет прикинуть.
                        0
                        Сам недавно задался целью найти инструмент для чистки лишнего в node_modules, нашел вот такой инструмент, попробуйте — Modclean
                        +1
                        На будущее, когда кода будет много, можно еще добавить copy/paste detector, например jscpd.
                          0

                          Спасибо за статью, но назрел вопрос — неужели Вам удобно каждый раз печатать ./node_modules/.bin/...? Почему бы не запускать как npm run ... или npx ...?

                            0
                            Еще как неудобно! Но к своему стыду я не знал, что если запускать через npm run, то npm подтянет локальные, а не глобальные модули. А вот npx появился только с 5.2 версии и может не стоять на удаленном окружении.
                              0

                              Ну я про npx сказал больше к тому, что такая возможность существует :)

                              0

                              Ну и отсюда же рекомендация — в секции scripts package.json можно писать сразу название бинарника, который хотите запустить, без указания пути до него.

                                0
                                Так вроде исправил в статье, разве нет? Остались только пути к локальным инициализациям которые выполняются сразу после установки. Но это от просто для компактности что бы не делать три шага по установке вместо двух.
                                  +1

                                  Ну я поиском по node_modules нашел 4 неисправленных места. Инициализации же тоже можно делать через npm, например npm run tsc --init

                                    +1
                                    Три тысячи чертей!

                                    Вы правы. Пора в отпуск, а не статьи писать.
                                      0

                                      А у вас такой сценарий работает? Если не писать ничего в package.json


                                      npm i -D typescript
                                      npm run tsc

                                      Если да то какая ОС/командное окружение?

                                        0

                                        Ругается на отсутствие package.json, что логично. А у Вас часто возникают потребности запускать пакеты без инициализации окружения в директории?

                                          0
                                          Да нет, тут вопрос исключительно в упрощении статьи. Что бы не делать три шага а два.
                                0
                                Drag13 так а что такое call в вашем примере:
                                "prepush": "call npm run lint && call npm run test"
                                  0

                                  Call one batch program from another, or call a subroutine — link


                                  Просто такой кривой вызов из-под винды нескольких процессов.


                                  Выше megaboich писал что так тоже можно не делать.

                                  0
                                  del
                                    0

                                    ESlint очень приятно использовать в VSCode, включив автоисправление по сохранению.

                                      +1
                                      TsLint тоже так умеет, но надо смотреть что бы они не конфликтовали с prettier и/или c editorconfig.
                                      +1
                                      Для совсем уж перфекционизма, опубликовывать в npm можно через пакет github.com/shashkovdanil/clean-publish, который перед публикацией очистит все dev поля и скрипты из package.json (devDependencies, prettier, babel и т.д.)
                                        0
                                        Тут и так статья с достаточно большим оверхедом, а это была бы просто изюминка. Но, конечно, спасибо за интересный пакет, я такой еще не встречал)
                                          0
                                          Данный пакет по идее ничего не усложняет. Просто публикация пакета будет не командой npm publish, а npx clean-publish
                                            0
                                            Не в плане сложности, а в плане технический манипуляций над проектом.

                                            И, к сожалению усложняет, т.к. теперь нужно знать что паблиш идет с помощью не стандартной команды.
                                        0
                                        По хорошему если уже решили использовать TypeScript, то проект может быть полностью javascript less, то есть совсем без js файлов. Да, karma конфиг может быть TS файлом. Если ничего не путаю, по моему опыту, karma-typescript не является обязательным.

                                        Также не совсем понятно почему именно karma. Если браузерное окружение для запуска тестов не нужно, то выбор гораздо шире (например ava). Jasmine тоже спорный выбор, предпочитаю когда все зависимости импортируются в явном виде.

                                        Ну и кончено вместо && в скриптах лучше использовать кросспратформенное решение, например модуль npm-run-all или аналоги.

                                        Only users with full accounts can post comments. Log in, please.