Проблема с просмотром SCSS

Есть язык SASS (SCSS), который великолепен. Есть node-sass, это круто. У него очень приятный функционал — --watch (-w):

node-sass -w input.scss -o build/

Он начинает отслеживать любые изменения в input.scss и перекомпилировать их в build/input.css. Это очень полезно в развитии.

Проблема

Так вот проблема: что, если я хочу сделать какие-то дополнительные действия после каждой перекомпиляции? Например. Я хочу применить Автопрефиксер к output.css.

Если бы это была обычная (не «просмотровая») подборка, я бы сделал:

node-sass input.scss | postcss -u autoprefixer ... > output.css

Но каналы POSIX (| …) не работают с процессами переднего плана, а также с перенаправлением файловых дескрипторов POSIX (> …). И, к сожалению, node-sass CLI не предоставляет функций постобработки (начиная с версии 4.1.1).

Существующие решения

Существует несколько решений, основанных на следующем подходе:

  1. Следите за изменениями в src/*.scss с помощью внешнего инструмента наблюдения (например, nodemon, watch и т. д.).
  2. Делайте обычную компиляцию изменения (чтобы мы могли сцепить несколько действий).

Это нормально, когда у вас есть только один основной файл SCSS, который необходимо (повторно) скомпилировать.

Хотя и в этом случае подход не идеален — основной файл будет перекомпилирован при любых src/*.scss изменениях файла, даже если некоторые из них не используются в основном файле.

Но большая проблема возникает, когда у вас есть несколько основных файлов SCSS. Например, в Django есть несколько статических папок (по одной для каждого многократно используемого приложения Django), которые могут содержать файлы SCSS.

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

Анализ

Преимущество node-sass -w input.scss в том, что:

  • он следит за изменениями в input.scss
  • … и во всех включенных файлах
  • … и больше нигде

Так что это кажется лучшим способом просмотра. Но он не позволяет постобработку. Итак, как действовать?

Способ 1: интерфейс командной строки

Идея состоит в том, чтобы использовать node-sass --watch с postcss --watch:

node-sass -w src/input.scss -o tmp/
postcss -w -u autoperfixer ... tmp/input.css -o build/output.css

Вот полный пример:

mkdir -p build/
node-sass src/input.scss -o tmp/
node-sass -w src/input.scss -o tmp/ &
postcss -w -u autoprefixer \
  --autoprefixer.browsers="ie >= 9, > 1%" \
  tmp/input.css -o build/output.css &

Примечание. Я делаю две дополнительные вещи:

  1. mkdir -p build/ — иначе postcss -o build/output.css сломается.
  2. node-sass src/input.scss -o tmp/ — согласно данному тикету, node-sass больше не занимается первоначальной компиляцией. Без него postcss tmp/input.css не получится; а он нам нужен в любом случае.

Вы можете изменить подход и использовать $tmpdir=`mktemp -d` вместо tmp/, чтобы избежать конфликтов имен файлов.

Не так уж плохо, но выглядит немного хрупким и громоздким.

Способ 2: JavaScript

node-sass написан на JS, как и autoprefixer, так почему бы не использовать JS?

Увы, после прочтения исходного кода я понял, что --watch является частью node-sass клиента, а не node-sass библиотеки. Поэтому мы не можем использовать его повторно, нам нужно скопировать и вставить некоторый код JS из клиента.

Но сначала нам нужно понять, как это работает:

  • sass-graph получает все файлы, входящие в просматриваемый
  • взгляд следит за изменениями в файлах
  • node-sass lib отображает файлы при изменении

Вот упрощенный пример:

var fs = require('fs');
var sass = require('node-sass');
var grapher = require('sass-graph');
var Gaze = require('gaze');
// This probably should come from outside
var inputFile = 'src/input.scss';
var outputFile = 'build/output.css';
// Make sure 'build/' dir exists
try { fs.mkdirSync('build'); } catch(e) {}
// Renderer
function render() {
  var result = sass.renderSync({file: inputFile});
  fs.writeFile(outputFile, result.css.toString());
}

// Initial render
render();

// Get watched files
function getWatchedFiles() {
  var includedFiles = grapher.parseFile(inputFile);
  return Object.keys(includedFiles.index);
}

// Start watching
var gaze = new Gaze();
gaze.add(getWatchedFiles());
gaze.on('changed', function() {
  gaze.add(getWatchedFiles()); // update watched files index
  render();
});

Это только обычные функции просмотра SCSS. Добавим autoprefixer:

var fs = require('fs');
var sass = require('node-sass');
var grapher = require('sass-graph');
var Gaze = require('gaze');
var postcss = require('postcss');
var autoprefixer = require('autoprefixer');

// This probably should come from outside
var inputFile = 'src/input.scss';
var outputFile = 'build/output.css';

// Make sure 'build/' dir exists
try { fs.mkdirSync('build'); } catch(e) {}

// Renderer: SCSS -> Autoprefizer
function render() {
  sass.render({file: inputFile}, function(err, result) {
    var processor = postcss([
      autoprefixer({browsers: ['ie >= 9', '> 1%']})]
    );
    processor.process(result.css.toString()).then(function(result) {
      fs.writeFile(outputFile, result.css);
    });
  });
}

// Initial render
render();

// Get watched files
function getWatchedFiles() {
  var includedFiles = grapher.parseFile(inputFile);
  return Object.keys(includedFiles.index);
}

// Start watching
var gaze = new Gaze();
gaze.add(getWatchedFiles());
gaze.on('changed', function() {
  gaze.add(getWatchedFiles()); // update watched files index
  render();
});

Примечание: в этом примере изменена только render() функция.

ПЛЮСЫ: мы достигли желаемого:

  • мы смотрим исходный файл SCSS так же, как это делает node-sass
  • мы применяем autoprefixer при каждой перекомпиляции
  • делаем первоначальную компиляцию
  • и у нас есть только один процесс просмотра

ПРОТИВ: это был всего лишь демонстрационный пример:

  • он не обрабатывает ошибки
  • он не обрабатывает некоторые пограничные случаи (например, добавление/удаление зависимых файлов)
  • он не принимает необходимые аргументы извне
  • и требует изменений исходного кода для дополнительной постобработки

Также это выглядит немного сложно (несмотря на то, что уже упрощено).

Способ 3: новый интерфейс командной строки

Я хочу иметь интерфейс командной строки, который просматривает файл SCSS, как это делает node-sass, и позволяет выполнять любую постобработку при изменении. Что-то типа:

watch-my-sass src/input.scss -- "node-sass | postcss -u autoperfixer > build/output.css"

Я не нашел существующего решения, поэтому я собираюсь написать его.

Я мог бы внести свой вклад в node-sass (и, возможно, это лучший способ), но я не могу дождаться выхода новой версии, мне это нужно сейчас. И я предвижу другие трудности в этом :) Может быть, я (или ты?) сделаю это в будущем.

(пишу, пишу, пишу…) и вот он, новый блестящий интерфейс командной строки:
https://github.com/kottenator/node-sass-watcher

Просто несколько примеров того, как это работает:

node-sass-watcher src/input.scss -o build/output.css -c 'node-sass <input> | postcss -u autoprefixer --autoprefixer.browsers="> 1%"'

Кроме того, он также предоставляет JS API:

var Watcher = require('node-sass-watcher');
var watcher = new Watcher('src/input.scss');
// triggered once
watcher.on('init', render);
// triggered on the input file changes and all it's 
// dependecies changes, incl. file addition/deletion
watcher.on('update', render);
// start watching
watcher.run();

function render() {
  doSomethingWith('src/input.scss');
}

Я надеюсь, что это было полезно. Если вы знаете лучшее решение — поделитесь им в комментариях! ;)