devDependencies

Opublikowano 09 kwietnia 2020
5 minut

W świecie programistów są takie pytania, które od pokoleń dzielą ludzi. Spacja czy tabulatury? Klamerka w tej samej linijce czy w osobnej? Czy potrzebne są nam średniki? Nie dawno odkryłem kolejne tego typu pytanie, ale tym razem sprawa jest trochę bardziej skomplikowana.

Czym jest dependencies i devDependencies?

Akcja dotyczy JavaScriptu, a dokładniej Node.js. Każdy projekt Node’a zawiera package.json w którym są zapisane zależności do projektu, które potem instalujemy używając npm lub yarn. Są też tzw. devDependencies czyli zależności do paczuszek, które nie są potrzebne do działania aplikacji, ale przydają się programistom. Do tej kategorii można zaliczyć biblioteki do testów, lintery, formattery i inne pluginy jak np. husky czy lint-staged.

W przypadku Node.js, czyli aplikacji serwerowych (bo Node.js nie istnieje w przeglądarce) sprawa jest w miarę prosta. W dependencies są zależności, które muszą być na produkcyjnym serwerze a w devDependencies praktycznie cała reszta.

Frontend

Na frontendzie klasycznie nic nie może być proste.

Jeśli korzystasz z bundlera np. webpack lub pośrednio z bundlera czyli react-scripts, @vue/cli-service czy @angular/compiler-cli to ten podział zależności nie jest taki jasny. Wynika to z faktu, że z punktu widzenia Node.js produkcją nie jest przeglądarka a sam bundler i moment kompilacji. Bundler tworząc naszego builda nie przejmuje się tym co jest w package.json, a zależności rozwiązuje po importach w kodzie.

Z tego wynika, że w dependencies powinniśmy mieć wszystko co jest potrzebne do zbudowania aplikacji. TypeScript, node-sass czy nawet biblioteki do testowania, jeśli traktujemy testy jako konieczny etap budowania, powinny znaleźć się w sekcji dependencies.

Tak właśnie robi create-react-app:

"dependencies": {
  "@testing-library/jest-dom": "^4.2.4",
  "@testing-library/react": "^9.3.2",
  "@testing-library/user-event": "^7.1.2",
  "@types/jest": "^24.0.0",
  "@types/node": "^12.0.0",
  "@types/react": "^16.9.0",
  "@types/react-dom": "^16.9.0",
  "react": "^16.13.1",
  "react-dom": "^16.13.1",
  "react-scripts": "3.4.1",
  "typescript": "~3.7.2"
},

devDependencies są puste, ale mógłby w nim być np. dodany prettier czy husky.

Nie wszyscy jednak robią jak React

Oto wycinki z wygenerowanych package.json z oficjalnych CLI:

  • Angulara
"dependencies": {
  "@angular/animations": "~9.1.0",
  "@angular/common": "~9.1.0",
  "@angular/compiler": "~9.1.0",
  "@angular/core": "~9.1.0",
  "@angular/forms": "~9.1.0",
  "@angular/platform-browser": "~9.1.0",
  "@angular/platform-browser-dynamic": "~9.1.0",
  "@angular/router": "~9.1.0",
  "rxjs": "~6.5.4",
  "tslib": "^1.10.0",
  "zone.js": "~0.10.2"
},
"devDependencies": {
  "@angular-devkit/build-angular": "~0.901.0",
  "@angular/cli": "~9.1.0",
  "@angular/compiler-cli": "~9.1.0",
  "@angular/language-service": "~9.1.0",
  "@types/node": "^12.11.1",
  "@types/jasmine": "~3.5.0",
  "@types/jasminewd2": "~2.0.3",
  "codelyzer": "^5.1.2",
  "jasmine-core": "~3.5.0",
  "jasmine-spec-reporter": "~4.2.1",
  "karma": "~4.4.1",
  "karma-chrome-launcher": "~3.1.0",
  "karma-coverage-istanbul-reporter": "~2.1.0",
  "karma-jasmine": "~3.0.1",
  "karma-jasmine-html-reporter": "^1.4.2",
  "protractor": "~5.4.3",
  "ts-node": "~8.3.0",
  "tslint": "~6.1.0",
  "typescript": "~3.8.3"
}
  • Vue
"dependencies": {
  "core-js": "^3.6.4",
  "vue": "^2.6.11"
},
"devDependencies": {
  "@vue/cli-plugin-babel": "~4.2.0",
  "@vue/cli-plugin-eslint": "~4.2.0",
  "@vue/cli-service": "~4.2.0",
  "babel-eslint": "^10.0.3",
  "eslint": "^6.7.2",
  "eslint-plugin-vue": "^6.1.2",
  "vue-template-compiler": "^2.6.11"
},

Vue i Angular robi to inaczej, przy czym Angular jest trochę niekonsekwentny i @angular/compiler jest nagle w dependencies, gdzie w zamyśle mają się znaleźć tylko te paczki, które prawdopodobnie znajdą się w naszym bundlu. Prawdopodobnie, bo tak jak wyżej o tym wspomniałem—to importy mają znaczenie. Pewność możemy mieć tylko analizując bundla.

Chciałbym tutaj dodać jakąś sekcję plusów tego rozwiązania, ale poza wymienionym “jasnym” podziałem w pliku to żadnego nie ma. Technicznie to rozwiązanie jest nawet gorsze, bo tracimy możliwość odseparowania rzeczywiście niepotrzebnych paczek i np. nasze CI zawsze będzie pobierać huskiego i ustawiać hooki do gita.

Dlaczego tak się dzieje? Nie wiem, ale mogę się domyślać. Jednym z możliwych powodów jest po prostu brak wiedzy czym tak naprawdę jest Node.js i wynikający z tego chaos. Ludzie po prostu googlują dependencies vs devdependencies i dostają odpowiedź.

Biblioteki

Sprawa staje się jeszcze trudniejsza gdy weźmiemy pod uwagę biblioteki. Te powinny mieć jak najmniej dependencies a cały zestaw do testowania i budowania w devDependencies.

Oto np. zależności date-fns

"dependencies": {},
"devDependencies": {
  "@babel/cli": "^7.5.5",
  "@babel/core": "^7.5.5",
  "@babel/node": "^7.5.5",
  ...
  "babel-preset-power-assert": "^3.0.0",
  "cloc": "^2.2.0",
  "coveralls": "^3.0.6",
  "eslint": "^5.16.0",
  "eslint-config-prettier": "^4.3.0",
  "firebase": "^3.7.1",
  "flow-bin": "0.84.0",
  "fs-promise": "^1.0.0",
  "glob-promise": "^2.0.0",
  "gzip-size-cli": "^1.0.0",
  "husky": "^1.0.1",
  "istanbul-instrumenter-loader": "^3.0.1",
  "jest": "^24.8.0",
  "jest-plugin-context": "^2.9.0",
  "js-beautify": "^1.5.10",
  "jsdoc-to-markdown": "^5.0.0",
  "karma": "^3.1.4",
  ...
  "karma-webpack": "^4.0.2",
  "lint-staged": "^7.3.0",
  "lodash": "^4.17.15",
  "lodash.clonedeep": "^4.5.0",
  "mocha": "^3.5.3",
  "moment": "^2.24.0",
  "mz": "^2.7.0",
  "node-fetch": "^1.7.3",
  "power-assert": "^1.6.1",
  "prettier": "^1.18.2",
  "rimraf": "^2.7.1",
  "sinon": "^7.4.1",
  "size-limit": "^0.21.0",
  "snazzy": "^7.0.0",
  "systemjs": "^0.19.39",
  "systemjs-plugin-babel": "0.0.17",
  "typescript": "^3.7.2",
  "webpack": "4",
  "webpack-cli": "^3.1.2",
  "world-countries": "^1.8.1"
},

Wynika to z tego, że jeśli np. dodajesz zależność do Reacta to również pobierzesz zależności Reacta, ale już nie zależności deweloperskie. Nikt nie lubi marnować miejsca na dysku.

Wniosek: jeśli ktoś będzie polegał na twojej paczce to ograniczenie sekcji dependencies na pewno będzie na plus 😇. Podobnie powinny postępować moduły w mikrofrontendach opartych na importach, ale wiele zależy od konfiguracji.

Czy jest o co walczyć?

Pewnie nie, chociaż czasem sam przegląd package.json daje tyle informacji by stwierdzić czy ma się do czynienia z normalnym projektem 😕.

Frontend to taki hack, że takie szczegóły są niewiele warte, ale przynajmniej można pokłócić się w pracy.

Podsumowanie

  • Teoretycznie poprawnym rozwiązaniem jest zapisanie wszystkich potrzebnych zależności do zbudowania aplikacji w dependencies
  • Jeśli tak nie zrobisz to świat się nie zawali
  • Biblioteki powinny unikać dependencies
  • Nie ma potrzeby bić się o to kto ma racje, ale mieć swoje zdanie:

Zawsze Warto

Nie mogłem się powstrzymać.

Issue, który upewnił mnie w moich przekonaniach

Bartosz Wiśniewski

Everything* Developer, entuzjasta technologii Google, przyjaciel Wrocław JUG, majsterkowicz i piwowar. Zwolennik prostych i pragmatycznych rozwiązań.