npm scripts хороши не потому, что умеют все. Они хороши тем, что закрывают заметную часть сборки без лишнего уровня абстракции. Для небольшого и среднего фронтенд-проекта этого часто достаточно: ты видишь реальные CLI-команды, не прячешь их за отдельный DSL и не тащишь тяжелый task runner только ради пары шагов сборки.

Старые статьи про npm scripts часто быстро устаревают, потому что завязаны на пакеты вроде node-sass или на полуфрагменты package.json, которые невозможно запустить как есть. Ниже разберем современный минимальный набор: сборка CSS, linting, последовательный и параллельный запуск, watch-режим и встроенные pre/post hooks.

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

Почему npm scripts часто хватает

У scripts есть три сильные стороны:

  • они запускаются прямо из package.json, без дополнительной прослойки;
  • локальные бинарные файлы из node_modules/.bin доступны автоматически;
  • любой разработчик команды видит реальную команду, а не скрытую логику внутри task runner.

Если задача сводится к “запусти CLI с аргументами”, npm scripts обычно достаточно.

Базовый набор зависимостей

Для примеров из статьи используем такой набор:

npm install -D sass postcss postcss-cli postcss-preset-env stylelint eslint npm-run-all onchange browser-sync

Обрати внимание: node-sass больше не нужен. Его давно заменил пакет sass, основанный на Dart Sass.

Минимальная настройка PostCSS может выглядеть так:

module.exports = {
  plugins: [require("postcss-preset-env")()],
};

Сборка CSS через Sass и PostCSS

Начнем с самого частого сценария: собрать Sass в CSS, а затем прогнать результат через PostCSS.

{
  "scripts": {
    "build:css": "sass src/scss:dist/css --style=compressed --no-source-map && postcss dist/css/*.css --replace"
  }
}

Что делает этот скрипт:

  • sass src/scss:dist/css компилирует все Sass-файлы из src/scss в dist/css;
  • --style=compressed сразу сжимает результат;
  • --no-source-map отключает sourcemap, если он не нужен;
  • postcss dist/css/*.css --replace берет получившиеся CSS-файлы и перезаписывает их результатом обработки.

Если хочешь сначала получить читаемый CSS, а уже потом минифицировать его отдельным шагом, разделяй эти задачи на две разные команды. Так дебажить проще.

Linting JavaScript и стилей

Скрипты удобно использовать и как тонкую оболочку над линтерами:

{
  "scripts": {
    "lint:js": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
    "lint:css": "stylelint \"src/scss/**/*.scss\""
  }
}

Важный момент: npm scripts не заменяют конфигурацию инструмента. Они только объявляют, как именно этот инструмент запускать в проекте. Поэтому сами правила ESLint и Stylelint лучше держать в отдельных конфигурационных файлах, а не пытаться впихнуть всю логику в одну команду.

Последовательный и параллельный запуск задач

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

Для этого удобен npm-run-all.

{
  "scripts": {
    "lint:js": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
    "lint:css": "stylelint \"src/scss/**/*.scss\"",
    "build:css": "sass src/scss:dist/css --style=compressed --no-source-map && postcss dist/css/*.css --replace",
    "build": "run-s lint:js lint:css build:css"
  }
}

run-s запускает задачи последовательно. Это полезно, когда следующий шаг имеет смысл только после успешного завершения предыдущего.

Если задачи независимы и должны жить одновременно, используй run-p:

{
  "scripts": {
    "serve": "browser-sync start --server dist --files \"dist/**/*\"",
    "watch:css": "onchange \"src/scss/**/*.scss\" -- npm run build:css",
    "dev": "run-p serve watch:css"
  }
}

Watch-режим и локальный сервер

Есть два типовых варианта watch-режима:

  1. использовать встроенный --watch у самого инструмента;
  2. отслеживать файлы отдельно и на изменения запускать нужный скрипт.

Второй вариант универсальнее, поэтому для примера используем onchange:

{
  "scripts": {
    "build:css": "sass src/scss:dist/css --style=compressed --no-source-map && postcss dist/css/*.css --replace",
    "watch:css": "onchange \"src/scss/**/*.scss\" -- npm run build:css",
    "serve": "browser-sync start --server dist --files \"dist/**/*\"",
    "dev": "run-p watch:css serve"
  }
}

Такой набор делает три вещи:

  • собирает CSS по запросу;
  • следит за изменениями в исходниках;
  • поднимает локальный сервер и обновляет браузер при изменениях в dist.

Для простого фронтенд-проекта этого часто достаточно без Webpack, Gulp или отдельного dev server.

Pre и post hooks

npm автоматически понимает скрипты с префиксами pre и post.

Если у тебя есть основной скрипт build, npm выполнит prebuild перед ним и postbuild после него.

{
  "scripts": {
    "prebuild": "npm run lint:js && npm run lint:css",
    "build": "npm run build:css",
    "postbuild": "npm run serve"
  }
}

Это полезно, когда до основной сборки нужно выполнить предсказуемую подготовку. Но злоупотреблять этими хуками не стоит: если цепочка запуска не очевидна из названия, отладка становится неприятнее.

Когда npm scripts уже мало

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

Признаки, что пора выносить логику выше:

  • в скриптах много shell-магии и OS-зависимых конструкций;
  • одна команда растягивается на полэкрана;
  • появляются сложные ветвления и вычисления;
  • нужно переиспользовать одну и ту же процедурную логику в нескольких местах.

В этот момент лучше написать маленький Node-скрипт и запускать уже его из npm run, а не продолжать усложнять shell-команды.

Итог

npm scripts отлично работают как легкий orchestration-слой над готовыми CLI. Они особенно хороши там, где проекту нужны понятные шаги сборки, linting и локальная разработка, но не нужен отдельный инструмент ради самого инструмента.

Если нужен рабочий минимум, начни с четырех вещей: build:css, lint:*, dev и одного-двух watch-скриптов. Этого достаточно, чтобы поддерживать небольшой фронтенд-проект в порядке без лишней инфраструктуры.