NPM Scripts - przykładowa konfiguracja

Przykład użycia

W poprzednim rozdziale rozmawialiśmy sobie o npm i skryptach, które możemy tworzyć w pliku package.json.

Najczęściej będziemy je wykorzystywać w prostych sytuacjach, jak odpalanie danej paczki z kilkoma parametrami - ot by nie musieć za każdym razem wpisywać wszystkiego z palca.

Nie znaczy to jednak, że nie możemy ich wykorzystać do stworzenia całej konfiguracji dla naszego projektu. Dość często spotkać można tezę, że w dzisiejszych czasach nie musimy używać dodatkowych narzędzi, a zamiast ich całą konfigurację możemy przerzucić na właśnie skrypty. W poniższym tekście spróbujemy zawalczyć z tym wyzwaniem.

Struktura projektu

Powiedzmy, że mamy projekt, który ma poniższą strukturę plików:


src
├── scss
│    └── main.scss
├── js
│    ├── app.js
│    └── other.js
└── images
     ├── image1.jpg
     └── image2.png

dist
├── css
├── js
├── images
└── index.html

W katalogu src będą znajdować się pliki, które będziemy edytować, natomiast do katalogu dist będzie trafiał zoptymalizowany wynik naszego kodu. I tak gdy będziemy pisać scss, wygenerowany zostanie plik dist/css/main.css. Gdy będziemy edytować pliki js, zostanie wygenerowany plik dist/js/app.js. Dodatkowo gdy zapiszemy jakieś pliki w katalogu src/images, ich zoptymalizowaną wersję nasz mechanizm zapisze w katalogu dist/images. Tak przynajmniej byśmy chcieli.

Po pierwsze stwórzmy odpowiednie pliki i katalogi. W domyślnym terminalu Windowsa nie ma domyślnie komendy touch służącej do tworzenia nowych plików, dlatego możemy dla pewności użyć odpowiedniego pakietu:


mkdir src dist && mkdir src/scss src/js src/images
npx touch dist/index.html src/scss/main.scss src/js/app.js

Następnie stwórzmy plik package.json komendą:


npm init -y

Konwersja SCSS do CSS

Pierwszą rzeczą jaką chcielibyśmy zrobić to skompilować pliki scss do katalogu dist/css.

Podejścia do tego jest minimum milion. My w ramach ćwiczenia spróbujmy użyć czystych pakietów bez bundlerów i podobnych narzędzi.

Jednym z pakietów służących do takiej zamiany jest pakiet sass. Przez wiele lat używany był pakiet node-sass, ale autor zaleca już używanie nowszej wersji wspierającej Dart Sass.

Po pierwsze zainstalujmy dany pakiet w naszym projekcie:


npm i sass -D

Od teraz możemy go odpalać poleceniem:


npx sass src/scss/main.scss dist/css

Zgodnie z opisem ze strony https://sass-lang.com/documentation/cli/dart-sass możemy dodać też dodatkowe parametry, takie jak skompresowanie wynikowego pliku czy też śledzenie zmian:


npx sass src/scss/main.scss dist/css/main.css --style=compressed --watch

Ja umyślnie zrezygnuję z tego drugiego, ponieważ przeszkadzał by nam w późniejszej konfiguracji. Jeżeli jednak powyższe polecenie tobie wystarczy w danym projekcie - droga wolna. Właśnie zakończyłeś konfigurację. Dość często przy mniejszych projektach jest to w zupełności wystarczające.

Żeby za każdym razem nie wpisywać powyższego polecenia, stwórzmy w package.json odpowiedni skrypt:


{
    "name": "test-project",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "sass": "sass src/scss/main.scss dist/css/main.css --style=compressed",
    },
    "devDependencies": {
        ...
    }
}

Od tej pory odpalimy go poleceniem:


npm run sass

Autoprefixer

Kolejnym pakietem, który nam się przyda to autoprefixer. Służy on do automatycznego dodawania prefixów do css. Jego działanie możesz zobaczyć bezpośrednio w tym demie.

Jako, że my nie używamy specjalnych narzędzi, zainstalujemy go zgodnie z opisem z sekcji cli (w poniższej linii dodałem postcss, którego brakuje w opisie na stronie autoprefixera):


npm install postcss postcss-cli autoprefixer

Żeby użyć go naszym projekcie bezpośrednio z termiala musielibyśmy wpisać:


npx postcss dist/css/*.css --use autoprefixer -r

Odpowiednie parametry możesz zobaczyć w opisie postcss.

Znowu długi zapis, dodajmy więc kolejny skrypt:


{
    "name": "test-project",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "autoprefixer": "postcss dist/css/*.css --use autoprefixer -r",
        "sass": "sass src/scss/main.scss dist/css/main.css --style=compressed"
    },
    "devDependencies": {
        ...
    }
}

Sam autoprefixer pozwala podać przeglądarki dla których powinny być generowane prefixy. Możesz o tym przeczytać tutaj. Wykorzystywany jest do tego mechanizm BrowserList. Gdy wejdziesz na tą stronę zobaczysz w początkowej fazie opisu, że przeglądarki możemy zdefiniować na dwa sposoby. Albo stworzymy plik .browserlistrc, albo dodamy odpowiedni zapis do pliku package.json. Ten plik już mamy, więc ja wybiorę tę drugą opcję. Z przeglądarek zostawiłem tylko "defaults", ponieważ pokrywa on przeglądarki, które mnie interesują:


{
    "name": "test-project",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "autoprefixer": "postcss dist/css/*.css --use autoprefixer -r"
        "sass": "sass src/scss/main.scss dist/css/main.css --style=compressed",
    },
    "browserslist": [
        "defaults"
    ],
    "devDependencies": {
        ...
    }
}

Optymalizacja grafik

W przypadku grafik użyjemy pakietu imagemin. Tak jak poprzednio zainstalujmy go i dodajmy odpowiednie skrypty do package.json:


npm i imagemin

{
    "name": "test-project",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "autoprefixer": "postcss dist/css/*.css --use autoprefixer -r",
        "images": "imagesmin \"src/images/* --out-dir=dist/images\"",
        "sass": "sass src/scss/main.scss dist/css/main.css --style=compressed",
    },
    "devDependencies": {
        ...
    }
}

Javascript

Ostatnia rzecz jaką się zajmiemy to sam Javascript. Wykorzystamy tutaj uglify-js.


npm install uglify-js -D

{
    "name": "test-project",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "autoprefixer": "postcss dist/css/*.css --use autoprefixer -r",
        "images": "imagesmin \"src/images/* --out-dir=dist/images\"",
        "sass": "sass src/scss/main.scss dist/css/main.css --style=compressed",
        "uglify": "uglifyjs src/js/*.js - -o dist/js/app.js && uglifyjs src/js/*.js -m -c -o dist/js/app.min.js"
    },
    "devDependencies": {
        ...
    }
}

Dodatkowo możemy dodać pakiet eslint, który będzie wytykał nam błędy w naszym kodzie.

Wpierw go zainstalujmy:


npm i eslint -D

Żeby eslint zadziałał jak należy, musimy utworzyć jego plik konfiguracyjny. W terminalu musimy wpisać eslint --init. Zostaniemy zapytani o serię pytań, na które musimy odpowiedzieć. Na pytanie o instalację wymaganych paczek, odpowiedz Yes. Jeżeli gdzieś się pomylisz, możesz przerwać klawiszami Ctrl + C i zacząć od nowa. Spokojnie też możesz ten proces powtórzyć.

Żeby teraz odpalić sprawdzenie kodu bezpośrednio z terminala wpiszemy:


npx eslint src/js

Dodajmy więc odpowiedni skrypt do naszego pliku:


{
    "name": "test-project",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "autoprefixer": "postcss dist/css/*.css --use autoprefixer -r",
        "images": "imagesmin \"src/images/* --out-dir=dist/images\"",
        "lint": "eslint src/js -s",
        "sass": "sass src/scss/main.scss dist/css/main.css --style=compressed",
        "uglify": "uglifyjs src/js/*.js -m -o dist/js/app.js && uglifyjs src/js/*.js -m -c -o dist/js/app.min.js"
    },
    "devDependencies": {
        ...
    }
}

Dodatkowa flaga -s oznacza, że dane zadanie będzie odpalone "cicho". EsLint po wykryciu błędów w naszych plikach Javascript pokaże je w terminalu, ale równocześnie wyemituje błąd. Taka była decyzja autorów. Żeby tego uniknąć zastosujemy właśnie tą flagę.

Grupowanie zadań

SCSS i Javascript mają przypisane po dwa zadania. Stwórzmy więc zadania grupujące, które po odpaleniu będą wykonywać wszystkie odpowiednie czynności:


{
    "name": "test-project",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "autoprefixer": "postcss dist/css/*.css --use autoprefixer -r",
        "images": "imagesmin \"src/images/* --out-dir=dist/images\"",
        "lint": "eslint src/js -s",
        "sass": "sass src/scss/main.scss dist/css/main.css --style=compressed",
        "uglify": "uglifyjs src/js/*.js -m -o dist/js/app.js && uglifyjs src/js/*.js -m -c -o dist/js/app.min.js",
        "build:all": "npm run build:css && npm run build:images && npm run build:sass",
        "build:css": "npm run sass && npm run autoprefixer",
        "build:images": "npm run images",
        "build:js": "npm run lint && npm run uglify"
    },
    "devDependencies": {
        ...
    }
}

Obserwowanie zmian w plikach

Problem z naszą konfiguracją jest taki, że powyższe skrypty będziemy musieli odpalać za każdym razem gdy wprowadzimy jakieś zmiany w scss. Gdybyśmy mieli tylko skrypt scss, wystarczyło by dodać do niego parametr --watch. Mamy niestety i inne zadania. Żeby umożliwić im obserwowanie zmian w plikach zainstalujmy pakiet onchange. Służy on do nasłuchiwania zmian w plikach. Jeżeli takie znajdą, zostaną odpalone zdefiniowane przez nas polecenia.


npm i onchange -D

Wykorzystajmy go więc w skryptach obserwujących:


{
    "name": "test-project",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "autoprefixer": "postcss dist/css/*.css --use autoprefixer -r",
        "images": "imagesmin \"src/images/* --out-dir=dist/images\"",
        "lint": "eslint src/js -s",
        "sass": "sass src/scss/main.scss dist/css/main.css --style=compressed",
        "uglify": "uglifyjs src/js/*.js -m -o dist/js/app.js && uglifyjs src/js/*.js -m -c -o dist/js/app.min.js",
        "build:all": "npm run build:css && npm run build:images && npm run build:sass",
        "build:css": "npm run sass && npm run autoprefixer",
        "build:images": "npm run images",
        "build:js": "npm run lint && npm run uglify",
        "watch:css": "onchange \"src/scss/**/*.scss\" -- npm run build:css",
        "watch:images": "onchange \"src/images/* -- npm run build:images\"",
        "watch:js": "onchange \"src/js/**/*.js\" -- npm run build:js",
    },
    "devDependencies": {
        ...
    }
}

Równoczesne skrypty i skrypt start

W powyższych skryptach korzystaliśmy ze składni &&. Służy ona do odpalania poleceń jeden po drugim.

Aby odpalić zadania równocześnie potrzebujemy dodatkowej funkcjonalności npm-run-all. Zainstalujmy ją poleceniem:


npm i npm-run-all -D

a następnie użyjmy w nowym skrypcie. Idealnie nada się tutaj skrypt start:


{
    "name": "test-project",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "autoprefixer": "postcss dist/css/*.css --use autoprefixer -r",
        "images": "imagesmin \"src/images/* --out-dir=dist/images\"",
        "lint": "eslint src/js -s",
        "sass": "sass src/scss/main.scss dist/css/main.css --style=compressed",
        "uglify": "uglifyjs src/js/*.js -m -o dist/js/app.js && uglifyjs src/js/*.js -m -c -o dist/js/app.min.js",
        "build:all": "npm run build:css && npm run build:images && npm run build:sass",
        "build:css": "npm run sass && npm run autoprefixer",
        "build:images": "npm run images",
        "build:js": "npm run lint && npm run uglify",
        "watch:css": "onchange \"src/scss/**/*.scss\" -- npm run build:css",
        "watch:images": "onchange \"src/images/* -- npm run build:images\"",
        "watch:js": "onchange \"src/js/**/*.js\" -- npm run build:js",
        "start": "npm-run-all --parallel watch:css watch:images watch:js"
    },
    "devDependencies": {
        ...
    }
}

I w zasadzie tyle. Po odpaleniu naszej konfiguracji poleceniem npm start jesteśmy gotowi do pracy.

Nie jest to konfiguracja idealna, wręcz można powiedzieć, że trochę przekombinowana?. Podejść do budowania własnego środowiska jest tyle co ludzi i ich opinii. Niektórzy twierdzą, że w dzisiejszych czasach stosowanie skryptów w zupełności wystarczy. I owszem - wydaje się, że przy prostych zastosowaniach jak kompilacja samego scss się one sprawdzają. Jednak jak widzisz przy nieco bardziej wymagających konfigach zaczynają się schodki. Osobiście przy tak dużej konfiguracji wolałbym jednak pozostać przy Gulpie czy innych rozwiązaniach. Kto co woli...

Browsersync - prawie

Trochę dłuższe Postscriptum.

Zauważ, że brakuje nam tutaj automatycznego odświeżania strony. Początkowo dodałem do tej konfiguracji BrowserSync.

Jest to narzędzie, które umożliwia automatyczne odświeżanie strony. Teoretycznie podobną funkcjonalność daje wspominany gdzieś na początku live-server, ale BrowserSync jest w tej materii zwyczajnie najlepszy. Nie tylko odświeża naszą stronę po wykryciu zmian w plikach, ale też udostępnia nam synchroniczne przeglądanie strony nad którą pracujemy. Co to oznacza? Po jej odpaleniu udostępnia nam dodatkowy adres, na który możemy się połączyć dowolnym urządzeniem z nasze sieci Wi-fi. Wszystkie urządzenia na których będziemy wyświetlać naszą stronę, będą to robić synchronicznie - to znaczy, że jeżeli przewinę stronę na ekranie komputera, przewinę ją na telefonie. Gdy kliknę przycisk na telefonie, kliknę go też na desktopie. Dodatkowo stylowanie nie musi powodować odświeżenia całej strony, bo BrowserSync może nowe style wstrzykiwać bez przeładowania.

Niestety - jeżeli w przypadku Gulpa czy Webpacka użycie BrowserSync nie jest raczej problemem, tak w powyższej konfiguracji BrowserSync działać działa, ale nie do końca tak jakbyśmy chcieli, ponieważ przy każdej zmianie podwójnie odświeża stronę. Wynika to z faktu, że odświeżanie odbywa się po wykrytych zmianach w plikach. W naszym przypadku wpierw kompilujemy scss generując nowy plik css, a następnie do już wygenerowanych plików dodajemy prefixy, co też powoduje zmianę. Gulp i podobne narzędzia najczęściej takie operacje przeprowadzają w pamięci, co jest szybsze ale też nie powoduje emitowania zmiany pliku.

Jeżeli wada ta ci nie przeszkadza - śmiało - możesz dodać i to narzędzie. Browsersync możemy zainstalować globalnie i dość łatwo odpalać z każdego miejsca za pomocą komend terminala (warto zrobić sobie do tego alias). W naszym przypadku zainstalujemy go poleceniem:


npm i browser-sync -D

Żeby ją teraz odpalić go w termialu wpiszemy:


npx browser-sync start --server dist --files "dist/*.html, dist/css/*.css, dist/js/*.js"

Tak jak poprzednio dodajmy odpowiedni skrypt w package.json:


{
    "name": "test-project",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "autoprefixer": "postcss dist/css/*.css --use autoprefixer -r",
        "lint": "eslint src/js -s",
        "sass": "sass src/scss/main.scss dist/css/main.css --style=compressed",
        "serve": "browser-sync start --server dist --files \"dist/*.html, dist/css/*.css, dist/js/*.js\"",
        "uglify": "uglifyjs src/js/*.js - -o dist/js/app.js && uglifyjs src/js/*.js -m -c -o dist/js/app.min.js",
        "build:all": "npm run build:css && npm run build:images && npm run build:sass",
        "build:css": "npm run sass && npm run autoprefixer",
        "build:images": "imagesmin \"src/images/* --out-dir=dist/images\"",
        "build:js": "npm run lint && npm run uglify",
        "watch:css": "onchange \"src/scss/**/*.scss\" -- npm run build:css",
        "watch:images": "onchange \"src/images/* -- npm run build:images\"",
        "watch:js": "onchange \"src/js/**/*.js\" -- npm run build:js",
        "start": "npm-run-all --parallel watch:css watch:images watch:js serve"
    },
    "devDependencies": {
        ...
    }
}

Gotowa konfiguracja

Konfigurację z powyższego tekstu znajdziesz tutaj.

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.