Blog

Setup projektu

Budujemy aplikacjęProgramowanie

#projekt#kodowanie#krok po kroku#setup

Опубліковано 11.12.2024

Setup projektu

Setup, czyli ustawienia najważniejszych rzeczy, jakich będziemy używać w trakcie budowy aplikacji. Dotyczy to tak konfiguracji najważniejszych bibliotek, jak i przygotowania pewnych założeń, nazwijmy to, wizualnych. Innymi słowy - przyjmiemy pewne konwencje pisania kodu, których będziemy się trzymać do końca. A konkretnie - skonfigurujemy narzędzia, które tych konwencji będą w dużej mierze pilnowały za nas.

"Hamlet?! To znowu ty?! Jak ja cię nie lubię..."

W moich projektach setup zazwyczaj wygląda bardzo podobnie. I tu ciekawa kwestia - chociaż za każdym razem robię praktycznie to samo, to również prawie za każdym razem coś nie działa 😂

Ale zanim przejdziemy do walki z naturą rzeczy, omówmy sobie pokrótce co będziemy konfigurować.

  • ESLint
  • Prettier
  • Testy
  • Storybook

Ale najsampierw

Dobrą praktyką jest, żeby kodu nie wrzucać bezpośrednio na główny branch, czyli w naszym przypadku main. Może w przypadku własnych, mniejszych projektów nie jest to konieczne, ale moim zdaniem nadal warto stosować takie podejście. Pozwoli to na wyrobienie sobie pewnego cennego nawyku.

Dlatego w pierwszym kroku utworzymy branch deweloperski, który nazwiemy (jakże oryginalnie) develop:

git switch -c develop

//albo starszym sposobem:

git checkout -b develop

//w przyszłości będę używał polecenia switch, ale nic nie stoi na przeszkodzie, żebyście nadal używali poleceń checkout czy branch

Następnie warto wysłać nasz pusty brach na zdalne repo na GitHubie:

git push

Aaaaa... co tu się stało?

fatal: The current branch develop has no upstream branch.

Otóż najpierw musimy powiązać nasz branch z repozytorium zdalnym. Na szczęcie git jasno informuje nas, jak to zrobić:

To push the current branch and set the remote as upstream, use

    git push --set-upstream origin develop

Później, kiedy będziemy chcieli wysłać zmiany lokalne na serwer, to wystarczy już jedynie samo git push. Podobnie, jeśli usuniemy branch w repozytorium zdalnym i ponownie je wyślemy, to także zrobimy to samym git push. Ale na razie wpisujemy w konsolę

git push --set-upstream origin develop

i gotowe, mamy branch deweloperski. Polecam też ustawienie go jako główny branch na GitHubie. Wówczas unikniemy ryzyka, że przez pomyłkę kolejne branche będziemy mergować z main zamiast z develop.

Dobrze, przechodzimy do konfiguracji projektu. Ja w tym celu utworzę kolejny branch. Będąc na branchu develop wpisujemy w konsoli

git switch -c setup && git push --set-upstream origin setup

W ten sposób najpierw powstał lokalnie nowy branch o nazwie setup, a następnie wysłaliśmy go do zdalnego repozytorium.

ESLint

Czym jest ESLint i dlaczego chcemy mieć go w projekcie?

Linter to narzędzie do statycznej analizy kodu. Pozwala on na identyfikację problematycznych fragmentów w kodzie. Jego ogromną zaletą jest możliwość dostosowania reguł. Mamy też możliwość wykorzystać (i także dopasować do siebie) gotowe zasady opracowane przez innych programistów.

ESLint pomaga utrzymać jakość kodu oraz automatycznie rozwiązać problemy ze stylem kodowania. Obsługuje aktualne standardy ECMAScript. Dzięki użyciu odpowiednich pluginów możemy także sprawdzać poprawność kodu pisanego w TS czy składni JSX/TSX.

Tyle wstępu, przejdźmy zatem do praktyki.

We wpisie poświęconym wyborowi technologii zapomniałem dodać, że aplikacja będzie pisana w TypeScripcie. Musimy zatem zadbać, żeby nasz linter poprawnie działał nie tylko z reactem, lecz także z TS-em.

Tutaj mała uwaga - tworząc projekt w Next.js domyślnie mamy już zainstalowany linter. Aby to sprawdzić, możemy podejrzeć plik package.json, w którym znajdziemy w sekcji devDependencies poniższe linijki:

"eslint": "^8",
"eslint-config-next": "14.2.3"

Jak widzimy, mamy już ESLinta w wersji 8 wraz konfiguracją pod NextJS w wersji 14.2.3.

Nie zaszkodzi jednak upewnić się, że paczki są aktualne i zainstalować wszystkiego ręcznie.

npm i -D eslint @next/eslint-plugin-next && npm i -D eslint-config-airbnb --legacy-peer-dep

Mamy aktualną wersję naszego lintera wraz z pluginem do NextJS i konfiguracją pod formatowanie zgodne z airbnb (tak, to ci od wynajmu krótkoterminowego). Teraz nasz plik package.json w sekcji devDependencies powinien zawierać poniższe linijki:

"@next/eslint-plugin-next": "^15.0.3",
"eslint": "^8.57.1",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-next": "14.2.3",

To tyle? Oczywiście, że nie. Teraz będziemy nasz linter konfigurować. W tym celu otwieramy plik .eslintrc.json, który aktualnie powinien wyglądać tak:

{
  "extends": "next/core-web-vitals"
}

Kod zastępujemy poniższym:

{
  "env": {
    "node": true,
    "es2021": true,
    "browser": true
  },
  "globals": {
    "React": true
  },
  "extends": [
    "airbnb",
    "next/typescript",
    "eslint:recommended",
    "next/core-web-vitals"
  ],
  "parserOptions": {
    "ecmaVersion": 12,
    "sourceType": "module",
    "ecmaFeatures": { "jsx": true }
  },
  "rules": {
    "no-shadow": "off",
    "arrow-body-style": "off",
    "import/extensions": "off",
    "curly": ["error", "multi"],
    "react/button-has-type": "off",
    "react/react-in-jsx-scope": "off",
    "react/jsx-filename-extension": "off",
    "import/prefer-default-export": "warn",
    "semi": 0,
    "react/function-component-definition": "off",
    "indent": "off",
    "max-len": "off",
    "comma-dangle": "off",
    "no-redeclare": "off",
    "react/jsx-indent": "off",
    "no-unused-vars": "off",
    "operator-linebreak": "off",
    "object-curly-newline": "off",
    "function-paren-newline": "off",
    "implicit-arrow-linebreak": "off",
    "react/require-default-props": "off",
    "nonblock-statement-body-position": "off",
    "react/jsx-one-expression-per-line": "off"
  }
}

Gotowe? Teoretycznie. Żeby nasze reguły zadziałały poprawnie, należy jeszcze upewnić się, że mamy włączony linter w naszym edytorze kodu.

Tutaj znajdziecie opis, w jaki sposób zintegrować ESLint z VS Code oraz z WebStormem.

Prettier

Mamy już naszego lintera. Czas dodać do niego narzędzie o wiele mówiącej nazwie Prettier. Jest to formater kodu. Być może wydaje się, że to praktycznie to samo, co linter. Jednak jego głównym zadaniem nie jest, - jak w przypadku ESLinta - dbanie o poprawność składni, ale zapewnienie czytelności i ujednolicenie kodu. Jest to szczególnie ważne podczas pracy w zespole, ponieważ pomaga utrzymać spójny kod bez względu na to, kto go w danym momencie pisze.

Dobrze, zainstalujmy zatem Prettiera:

npm i -D prettier eslint-plugin-prettier eslint-config-prettier

Teraz należy nieco zmodyfikować konfigurację ESLinta, dodając w pliku .eslintrc.json następujące linijki:

  • w sekcji extends do tablicy dodajemy "prettier",
  • dodajemy sekcję plugins: "plugins": ["prettier"],
  • w sekcji rules dodajemy: "prettier/prettier": ["error", { "endOfLine": "auto" }].

Teraz nasz plik powinien wyglądać następująco:

{
  "env": {
    "node": true,
    "es2021": true,
    "browser": true
  },
  "globals": {
    "React": true
  },
  "extends": [
    "airbnb",
    "prettier",
    "next/typescript",
    "eslint:recommended",
    "next/core-web-vitals"
  ],
  "parserOptions": {
    "ecmaVersion": 12,
    "sourceType": "module",
    "ecmaFeatures": { "jsx": true }
  },
  "plugins": ["prettier"],
  "rules": {
    "no-shadow": "off",
    "arrow-body-style": "off",
    "import/extensions": "off",
    "curly": ["error", "multi"],
    "react/button-has-type": "off",
    "react/react-in-jsx-scope": "off",
    "react/jsx-filename-extension": "off",
    "import/prefer-default-export": "warn",
    "semi": 0,
    "react/function-component-definition": "off",
    "indent": "off",
    "max-len": "off",
    "comma-dangle": "off",
    "no-redeclare": "off",
    "react/jsx-indent": "off",
    "no-unused-vars": "off",
    "prettier/prettier": ["error", { "endOfLine": "auto" }],
    "operator-linebreak": "off",
    "object-curly-newline": "off",
    "function-paren-newline": "off",
    "implicit-arrow-linebreak": "off",
    "react/require-default-props": "off",
    "nonblock-statement-body-position": "off",
    "react/jsx-one-expression-per-line": "off"
  }
}

ESLitn zaktualizowany, czas skonfigurować Prettiera.

Tworzymy w głównym katalogu projektu plik .prettierrc:

{
  "tabWidth": 2,
  "semi": false,
  "useTabs": false,
  "singleQuote": true,
  "trailingComma": "es5"
}

Podobnie, jak w przypadku ESLinta, musimy jeszcze zintegrować go z edytorem, żeby można było korzystać z dobrodziejstw automatycznego formatowania.

Gotowe!

Lint-Staged i Husky

Jest to krok opcjonalny, ale gorąco zachęcam, żeby go wdrożyć.

Lint-Staged i Husky pozwalają wdrożyć pre-commit hooks, czyli instrukcje, które będą każdorazowo wykonywane przed commitem. Pozwoli to zapobiec sytuacji, kiedy chcielibyśmy zakomitować kod, który odbiega od ogólnego formatowania lub, co gorsze, zawiera jakieś błędy składni. Jeśli będzie to możliwe, to wszelkie niedociągnięcia zostaną automatycznie naprawione. Jeśli nie, to dostaniemy komunikat błędu i commit zostanie zatrzymany.

Lecimy z implementacją.

npm i -D lint-staged husky

Tworzymy plik konfiguracyjny .lintstagedrc.js:

const path = require('path')

const buildEslintCommand = (filenames) =>
  `next lint --fix --file ${filenames
    .map((f) => path.relative(process.cwd(), f))
    .join(' --file ')}`

module.exports = {
  // Type check TypeScript files
  '*/.(ts|tsx)': () => 'yarn tsc --noEmit',
  '*.{js,jsx,ts,tsx,json,md,prettierrc,css,scss}':
    'prettier --write --config .prettierrc',
  '*.{js,jsx,ts,tsx}': [buildEslintCommand],
}

i uruchamiamy automatyczną konfigurację Husky'ego:

npx husky init

W głównym katalogu projektu powinniśmy zobaczyć nowy folder .husky. W nim znajdziemy plik pre-commit, który teraz otwórzmy. Powinien wyglądać następująco:

npm test

I tu zapewne pojawią się problemy. Ogólnie - to, co widzimy w pliku pre-commit zostanie wykonane przed każdym commitem. Spróbujmy zatem wpisać tę instrukcję w konsoli:

npm test

Efekt nie napawa optymizmem:

npm ERR! Missing script: "test"
npm ERR!
npm ERR! To see a list of scripts, run:
npm ERR!   npm run

npm ERR! A complete log of this run can be found in: xxx.log

Bez paniki, można to bardzo łatwo naprawić. Powyższy błąd mówi nam, że nie istnieje skrypt test. Możemy go zatem utworzyć w pliku package.json:

"scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "prepare": "husky",
    "test": "echo 'test'" //added line
},

Domyślnie tym skryptem będziemy uruchamiać testy, jednak w tym momencie nie mamy do tego narzędzia. Dlatego zwyczajnie wylogujemy słowo test.

Zobaczmy, jak to teraz działa.

npm test

> smoggy_foggy_2.0@0.1.0 test
> echo 'test'

test

Działa! Ale przecież nie po to konfigurowaliśmy Lint-Staged, żeby teraz go nie użyć. Dlatego edytujmy plik pre-commit, zastępując go poniższą instrukcją:

npx lint-staged --allow-empty

Zapisujemy wszystkie pliki i możemy przetestować nasze zmiany.

Zaczynamy od dodania zmian do gita. Następnie spróbujemy zrobić commit:

git add . //dodajemy wszystkie zmiany do stage'a
git commit -m"Configure project"

Chwila prawdy... i działa!

✔ Preparing lint-staged...
✔ Running tasks for staged files...
✔ Applying modifications from tasks...
✔ Cleaning up temporary files...
[setup 1447754] Configure project
 6 files changed, 693 insertions(+), 580 deletions(-)
 create mode 100644 .husky/pre-commit
 create mode 100644 .lintstagedrc.js
 create mode 100644 .prettierrc

Gratulacje, zadbaliśmy właśnie o poprawne działanie lintera i formatera oraz upewniliśmy się, że będą się one uruchamiały przed każdym commitem!

Rzut oka na package.json

W poprzednim akapicie wspomniałem o sekcji scripts w pliku package.json. Zakładam, że samego pliku package.json nie muszę przybliżać, dlatego skoncentruję się tylko na wspomnianej sekcji scripts.

W tym momencie wygląda ona następująco:

"scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "prepare": "husky"
},

Przeanalizujmy po kolei poszczególne skrypty.

  • dev: po wpisaniu w konsoli polecenia npm run dev spowoduje wykonanie polecenia next dev, które uruchamia środowisko deweloperskie,
  • build: polecenie npm run build wykona next build, które utworzy gotowy build, czyli wersję produkcyjną,
  • start: polecenie npm start uruchomi serwer produkcyjny,
  • lint: polecenie npm run lint uruchomi linter,
  • prepare: polecenie uruchomi Husky'ego.

Teraz w analogiczny sposób możemy tworzyć własne skrypty. Proponuję dodać ręczne uruchomienie Prettiera oraz Pre-Commit. Przynajmniej na początek.

Dodajemy zatem poniższe linijki:

"format": "prettier . --write",
"pre": "npx lint-staged --allow-empty"

Linijka z poleceniem pre jest dość jasna, bo wykonujemy manualnie to, co automatycznie uruchamia się przed każdym commitem. Nieco dokładniej postaram się wyjaśnić polecenie format.

Po wpisaniu w konsoli npm run format uruchomione zostanie polecenie prettier . --write. Jak nietrudno się domyślić komenda prettier uruchamia nasz formater. I tutaj posłużymy się dokumentacją Prettiera.

Use the prettier command to run Prettier from the command line.

prettier [options] [file/dir/glob ...]

W naszym przypadku kropka [.] odpowiada za blok [file/dir/glob ...], natomiast dodatkowo uruchamiamy Prettiera z flagą --write, która nadpisuje pliki zgodnie z przewidzianymi w konfiguracji regułami. Które pliki, zapytacie? Zgodnie z tym, co podaliśmy przed chwilą - wszystkie. Zaraz, ale gdzie to podaliśmy? Jak wspomniałem - kropka w poleceniu odpowiada za [file/dir/glob ...], podobnie jak np. w git add . informuje, że bierzemy pod uwagę wszystko, jak leci.

W kolejnych etapach jeszcze będziemy wracać do sekcji scripts, ale na razie wygląda ona następująco:

"scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "prepare": "husky",
    "format": "prettier . --write",
    "pre": "npx lint-staged --allow-empty"
},

Testy

Pisanie testów jednostkowych mało kto uważa za miłe i przyjemne. Niemniej są one niezwykle pomocne w procesie tworzenia i rozwijania aplikacji. Dlatego w naszym projekcie nie będziemy ich pomijać.

Jeśli ktokolwiek pracował już z testami w JS/TS, to zapewne domyśla się, że będziemy używać Jest.

Najpierw musimy zainstalować kilka paczek.

npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom ts-node

Następnie odpalamy konfigurację:

npm init jest@latest

Konfigurator zapyta nas po kolei o kilka rzeczy:

The following questions will help Jest to create a suitable configuration for your project

? Would you like to use Jest when running "test" script in "package.json"? › (Y/n)
? Would you like to use Typescript for the configuration file? › (y/N)
? Choose the test environment that will be used for testing › - Use arrow-keys. Return to submit.
    node
❯   jsdom (browser-like)
? Do you want Jest to add coverage reports? › (y/N)
? Which provider should be used to instrument code for coverage? › - Use arrow-keys. Return to submit.
    v8
❯   babel
? Automatically clear mock calls, instances, contexts and results before every test? › (y/N)

W moim przypadku odpowiedzi wyglądały następująco:

The following questions will help Jest to create a suitable configuration for your project

✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Would you like to use Typescript for the configuration file? … yes
✔ Choose the test environment that will be used for testing › jsdom (browser-like)
✔ Do you want Jest to add coverage reports? … yes
✔ Which provider should be used to instrument code for coverage? › babel
✔ Automatically clear mock calls, instances, contexts and results before every test? … yes

Dostajemy też informację, że zmodyfikowany został plik package.json. Zajrzyjmy więc do niego.

Widzimy w sekcji scripts nową pozycję:

"test": "jest"

Od teraz po wpisaniu w terminal npm run test uruchomiony zostanie Jest i będziemy mogli przetestować nasz kod. Ale o tym za chwilę.

Po konfiguracji otrzymaliśmy też powiadomienie o utworzeniu nowego pliku jest.config.ts, gdzie została zapisana konfiguracja Jesta.

Czy możemy już spokojnie uruchomić testy? No niby możemy, ale dostaniemy komunikat, że testy nie przeszły. Może nie wprost, bo w postaci informacji, że exiting with code 1. I treść tuż przed mówi jasno, z jakiego powodu: No tests found.

Niby to nie problem, możemy przecież nie uruchamiać testów przed utworzeniem jakiegokolwiek. Problem zacznie się, jeśli będziemy chcieć dodać polecenie npm run test do pliku pre-commit. A to właśnie będziemy teraz robić.

Aby rozwiązać nasz problem, możemy napisać jakiś test, który będzie zawsze przechodził pozytywnie. Tylko chyba nie do końca o to nam chodzi. Można też założyć (słusznie zresztą), że nie jesteśmy pierwszymi, którzy napotkali na taką niedogodność i poszukać odpowiedzi np. w dokumentacji. A możemy też dokładniej wczytać się w komunikat, jaki na wyświetlił Jest. Bo odpowiedź mamy przed sobą.

Zobaczmy jeszcze raz w konsolę:

No tests found, exiting with code 1
Run with `--passWithNoTests` to exit with code 0
In /home/gacek/Projects/smoggy_foggy_2.0
  10 files checked.
  testMatch: **/__tests__/**/*.[jt]s?(x), **/?(*.)+(spec|test).[tj]s?(x) - 0 matches
  testPathIgnorePatterns: /node_modules/ - 10 matches
  testRegex:  - 0 matches
Pattern:  - 0 matches

W drugiej linijce Jest jasno mówi, co zrobić w naszej sytuacji: Run with --passWithNoTests to exit with code 0

Wróćmy zatem do package.json i uzupełnijmy skrypt test o wymaganą flagę:

"scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "prepare": "husky",
    "test": "jest --passWithNoTests"
  },

i spróbujmy ponownie:

npm run test
No tests found, exiting with code 0
----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |       0 |        0 |       0 |       0 |
----------|---------|----------|---------|---------|-------------------

Sukces! Teraz możemy z czystym sumieniem przejść do pliku pre-commit, gdzie dodamy nasze testy. Plik po zmianach powinien wyglądać następująco:

npx lint-staged --allow-empty
npm run test

Teraz przed każdym commitem będziemy uruchamiać nie tylko linter i formater, ale też testy jednostkowe.

A co w sytuacji, jeśli z jakiegoś powodu chcemy puścić commit, nawet pomimo świadomości, że mamy gdzieś w kodzie błędy? W takiej sytuacji commitujemy z flagą --no-verify:

git commit -c"Commit comment" --no-verify

Trzeba tylko pamiętać, żeby nie nadużywać tej opcji i korzystać z niej świadomie.

Storybook

Storybook to bardzo przydatne narzędzie, pozwalające testować oraz prezentować pojedyncze komponenty w odizolowanym środowisku. W przypadku bardzo prostych elementów można się nawet pokusić o zastąpienie testów jednostkowych właśnie Storybookiem.

Storybook wspaniale też sprawdza się jako uzupełnienie dokumentacji, pozwala bowiem na bardzo przejrzyste opisanie i praktyczne zaprezentowanie komponentów wraz z dostępnymi opcjami i wariacjami.

Nie traćmy zatem czasu i zainstalujmy Storybooka:

npx storybook@latest init

Ważnym jest, żeby przed instalacją usunąć lub zakomentować polecenie prepare w pliku package.json!

Jeśli wszystko poszło dobrze, to w naszym projekcie powinny pojawić się dwa nowe katalogi: .storybook w katalogu głównym oraz stories w katalogu src.

Pierwszy z nich, czyli .storybook zawiera pliki konfiguracyjne, których póki co nie będziemy ruszać. Drugi natomiast zawiera przykładowe storiesy, które posłużą nam do zapoznania się z podstawami Storybooka.

Ponadto, jeśli spojrzymy do pliku package.json, to zobaczymy nowe polecenia w sekcji scripts:

"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"

Pierwsza uruchomi serwer deweloperski na porcie 6006, gdzie będziemy mogli podejrzeć naszego Storybooka. Druga utworzy build produkcyjny.

Uwaga! W moim przypadku podczas próby uruchomienia Storybooka pojawił się błąd Cannot find module 'ajv/dist/compile/codegen'. Pomogło zastosowanie się do tej instrukcji.

Sprawdźmy zatem, jak to wygląda na ten moment. Jeśli Storybook nie uruchomił się po instalacji, odpalamy go poniższym poleceniem:

npm run storybook

Storybook dev server

Jeśli to wasze pierwsze spotkanie ze Storybookiem, to polecam przejrzeć pliki, jakie znajdują się w utworzonym katalogu src/stories oraz sprawdzić dokumentację narzędzia. Tymczasem ja usuwam katalog z przykładowymi storiesami, bo finalnie będę umieszczał je w innym miejscu.

rm -fr src/stories/

Gotowe!

Słowo na koniec

To tyle, jeśli chodzi o wstępną konfigurację projektu. Może niektóre kroki wydają się na ten moment nieco bez sensu , ale uwierzcie - to, co dziś zrobiliśmy, zaowocuje później.

Jeśli chcecie podejrzeć efekty dzisiejszej pracy, to możecie przejść na branch setup repozytorium projektu.

Niezmiennie też zapraszam na mojego twixera.

Do następnego!