Wstęp Dlaczego Monorepo? Obecnie nie można zaprzeczyć szybkiej ewolucji rozwoju oprogramowania. Zespoły rosną, projekty są coraz bardziej złożone. Firmy przeznaczają znaczne zasoby na utrzymanie rozproszonej bazy kodu składającej się z wielu fragmentów. Wkracza monorepo — pojedyncze, ujednolicone repozytorium, które łączy cały kod. Monorepo, dalekie od trendu, stało się ostatnio podejściem architektonicznym do przechowywania całej bazy kodu w jednym miejscu. Zespoły uzyskują ulepszone współdzielenie kontekstu, płynną współpracę i narzędzie, które naturalnie zachęca do ponownego wykorzystywania kodu. Konfigurowanie obszarów roboczych Yarn Uwaga: W niniejszym artykule za każdym razem, gdy jest mowa o Yarn, chodzi konkretnie o Yarn v4 — najnowszą wersję oferującą rozszerzone możliwości i lepszą wydajność. Czym są obszary robocze Yarn? Obszary robocze to pakiety monorepo, często nazywane pakietami. Pomagają one zarządzać wieloma pakietami w jednym repozytorium bez wysiłku. Dzięki obszarom roboczym możesz: Łatwe udostępnianie zależności: Bezproblemowo udostępniaj wspólne zależności w ramach swojego projektu. Uprość zarządzanie zależnościami: Yarn automatycznie łączy lokalne pakiety, redukując duplikację i ułatwiając rozwój. Przyspiesz instalacje: Skorzystaj z optymalizacji wydajności Yarn i mechanizmów buforowania (tj. ). wbudowanej funkcji plug'n'play Popraw kontrolę nad Monorepo: Zdefiniuj (reguły) i wykorzystaj aby zachować spójność. ograniczenia dziesiątki dostępnych wtyczek, Chociaż Yarn jest wybranym menedżerem dla tego artykułu ze względu na swoją prostotę, szybkość i rozbudowane opcje konfiguracji - ważne jest, aby pamiętać, że właściwy wybór zależy od konkretnych potrzeb projektu, preferencji zespołu i ogólnego przepływu pracy. Na przykład i to inne nowoczesne narzędzia, które oferują szeroki zakres funkcji. PNPM Turborepo Konfiguracja początkowa Konfiguracja Yarn jest prostym procesem. Postępuj zgodnie z oficjalnym przewodnikiem, aby zainstalować i skonfigurować Yarn w swoim projekcie: . Yarn Installation Guide Po zakończeniu instalacji przejdźmy do konfiguracji. Ponieważ używamy plug'n'play, musisz upewnić się, że Twoje IDE poprawnie rozpoznaje zależności. Jeśli używasz VSCode, uruchom: # Typescript is required for VSCode SDK to set up correctly yarn add -D typescript@^5 yarn dlx @yarnpkg/sdks vscode Jeśli używasz innego edytora kodu, sprawdź dostępne zestawy SDK tutaj: . Yarn Editor SDKs W tym momencie możesz już zacząć używać Yarn. Organizacja struktury Monorepo Teraz, gdy menedżer pakietów jest skonfigurowany, czas zaprojektować skalowalną organizację projektu. Przejrzysta, dobrze zdefiniowana struktura nie tylko ułatwia nawigację po repozytorium, ale także promuje lepsze ponowne wykorzystanie kodu. W tym przykładzie podzielimy bazę kodu na trzy główne kategorie: : Aplikacje Klient: Zawiera ostateczne, możliwe do wdrożenia produkty klienckie. Serwer: Zawiera ostateczne, gotowe do wdrożenia produkty serwerowe. : Cechy Klient: Dla samodzielnych widżetów interfejsu użytkownika. Serwer: Dla samodzielnych elementów logiki biznesowej zaplecza. : Biblioteki Domy współdzielą kod, taki jak komponenty systemu projektowego, stałe, zasoby i narzędzia. Jest to strefa bezkontekstowa do przechowywania logiki wielokrotnego użytku. Aby zademonstrować moc tej struktury folderów, zacznijmy od dodania tych głównych folderów do listy obszarów roboczych Yarn. W swoim głównym pliku package.json dodaj następujące elementy: "workspaces": [ "apps/**", "features/**", "libs/**" ] Ta konfiguracja mówi Yarn, aby traktował pakiety w tych folderach jako pakiety lokalne. Kolejne instalacje zapewnią, że zależności dla każdego pakietu zostaną prawidłowo skonfigurowane i połączone. Bootstrapping bazy kodu W tej sekcji przejdziemy przez minimalny przykład bazy kodu, który ilustruje, jak uruchomić monorepo. Zamiast dołączać pełne fragmenty kodu, podam krótkie przykłady z linkami do kompletnych plików w . repozytorium utworzonym specjalnie na potrzeby tego artykułu Bootstrapping aplikacji serwera Zaczynamy od prostego do uwierzytelniania użytkowników. Ta aplikacja serwerowa udostępnia pojedynczy punkt końcowy ( ), który wykorzystuje handler z innego pakietu. Express API /auth/signIn import express from "express"; import cors from "cors"; import { signInHandler } from "@robust-monorepo-yarn-nx-changesets/sign-in-handler"; const app = express(); const port = process.env.PORT || 1234; app.use(express.json()); app.use( cors({ origin: process.env.CORS_ORIGIN || "http://localhost:3000", }) ); app.post("/auth/signIn", signInHandler); app.listen(port, () => { console.log(`Server is running at http://localhost:${port}`); }); Link do pakietu Jak widać, punkt końcowy używa handlera zaimportowanego z innego pakietu. To prowadzi nas do naszego następnego komponentu: funkcji serwera. /auth/signIn Funkcja serwera Bootstrapping Funkcja serwera obejmuje logikę uwierzytelniania. W tym pakiecie definiujemy obsługę logowania, która wykorzystuje współdzielone narzędzie walidacji z bibliotek. import type { RequestHandler } from "express"; import { passwordValidator, usernameValidator, } from "@robust-monorepo-yarn-nx-changesets/validator"; const signInHandler: RequestHandler = (req, res) => { if (!req.body) { res.status(422).send("Request body is missing"); return; } if (typeof req.body !== "object") { res.status(422).send("Request body expected to be an object"); return; } const { username, password } = req.body; const usernameValidationResult = usernameValidator(username); if (typeof usernameValidationResult === "string") { res .status(422) .send("Invalid username format: " + usernameValidationResult); return; } const passwordValidationResult = passwordValidator(password); if (typeof passwordValidationResult === "string") { res .status(422) .send("Invalid password format: " + passwordValidationResult); return; } // Emulate a successful sign-in if (username === "test" && password === "test1234") { res.status(200).send("Sign in successful"); return; } return res.status(422).send("Username or password is incorrect"); }; export default signInHandler; Link do pakietu To podejście podsumowuje logikę uwierzytelniania w ramach własnego pakietu, co pozwala na jego niezależne rozwijanie i utrzymywanie. Zwróć uwagę, jak narzędzia walidatora są importowane ze . współdzielonej biblioteki Bootstrapping aplikacji klienckiej Następnie przyjrzyjmy się stronie klienta. W naszej aplikacji klienckiej budujemy prostą witrynę, która umożliwia uwierzytelnianie użytkownika poprzez wywołanie API serwera. "use client"; import { SignInForm } from "@robust-monorepo-yarn-nx-changesets/sign-in-form"; const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:1234"; export default function Home() { const handleSubmit = async (username: string, password: string) => { const response = await fetch(`${API_URL}/auth/signIn`, { method: "POST", body: JSON.stringify({ username, password }), headers: { "Content-Type": "application/json", }, }); if (response.status === 200) { alert("Sign in successful"); return; } if (response.status === 422) { alert("Sign in failed: " + (await response.text())); return; } alert("Sign in failed"); }; return ( <div className="w-full h-screen overflow-hidden flex items-center justify-center"> <SignInForm onSubmit={handleSubmit} /> </div> ); } Link do pakietu W tym przykładzie komponent został zaimportowany z pakietu funkcji klienta, co prowadzi nas do ostatniego komponentu. SignInForm Funkcja klienta bootstrappingowego Pakiet funkcji klienta zapewnia formularz uwierzytelniania wraz ze wspólną logiką walidacji. Zapobiega to duplikowaniu kodu i zapewnia spójność. import { passwordValidator, usernameValidator, } from "@robust-monorepo-yarn-nx-changesets/validator"; interface SignInFormProps { onSubmit: (username: string, password: string) => void; } const SignInForm = ({ onSubmit }: SignInFormProps) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault(); const username = (event.currentTarget[0] as HTMLInputElement).value; const usernameValidationResult = usernameValidator(username); if (typeof usernameValidationResult === "string") { alert(usernameValidationResult); return; } const password = (event.currentTarget[1] as HTMLInputElement).value; const passwordValidationResult = passwordValidator(password); if (typeof passwordValidationResult === "string") { alert(passwordValidationResult); return; } onSubmit(username!, password!); }; return ( <form onSubmit={handleSubmit}> <input type="text" placeholder="Username" /> <input type="password" placeholder="Password" /> <button type="submit">Submit</button> </form> ); }; export default SignInForm; Link do pakietu Tutaj ponownie widzimy wykorzystanie z naszych współdzielonych bibliotek, co zapewnia scentralizowanie logiki walidacji i łatwą konserwację. walidatora To wszystko w naszym przykładzie minimalnej bazy kodu. Pamiętaj, że ten kod jest uproszczoną ilustracją mającą na celu zademonstrowanie podstawowej struktury i połączeń między aplikacjami, funkcjami i bibliotekami w monorepo. Możesz rozszerzyć te przykłady w razie potrzeby, aby dopasować je do konkretnych wymagań swojego projektu. Uruchamianie skryptów za pomocą NX Zarządzanie skryptami w monorepo może być trudne. Podczas gdy Yarn pozwala przy użyciu różnych warunków, może wymagać niestandardowego skryptowania w celu uzyskania bardziej szczegółowej kontroli. Tutaj właśnie pojawia się NX: zapewnia gotowe rozwiązanie do wydajnego, ukierunkowanego wykonywania skryptów. uruchamiać skrypty w wielu pakietach Wprowadzenie do NX NX to system kompilacji zoptymalizowany dla monorepozytoriów z zaawansowanymi możliwościami CI. Dzięki NX możesz: : Wykorzystaj współbieżność, aby przyspieszyć kompilacje. Efektywne wykonywanie zadań równolegle : Zrozum powiązania między pakietami i skryptami. Identyfikuj relacje zależności : unikaj powtarzającej się pracy poprzez buforowanie wyników. Buforuj wyniki wykonywania skryptu Rozszerz funkcjonalność za pomocą . Dostosuj zachowanie za pomocą wtyczek: bogatego ekosystemu wtyczek Celowane wykonywanie skryptów Aby wykorzystać możliwości NX, najpierw musimy utworzyć plik , aby zdefiniować zestaw reguł dla naszych skryptów. Poniżej znajduje się przykładowa konfiguracja: nx.json { "targetDefaults": { "build": { "dependsOn": [ "^build" ], "outputs": [ "{projectRoot}/dist" ], "cache": true }, "typecheck": { "dependsOn": [ "^build", "^typecheck" ] }, "lint": { "dependsOn": [ "^build", "^lint" ] } }, "defaultBase": "main" } Mówiąc prościej, ta konfiguracja oznacza: Zbudować Skrypt pakietu jest zależny od pomyślnego skompilowania jego zależności, a jego dane wyjściowe są buforowane. build Kontrola typu Skrypt pakietu zależy zarówno od skryptów kompilacji, jak i kontroli typu jego zależności. typecheck Szarpie Skrypt dla pakietu zależy zarówno od skryptów kompilacji, jak i lint jego zależności. lint Teraz dodajmy skrypty do : package.json "scripts": { "build:all": "yarn nx run-many -t build", "build:affected": "yarn nx affected -t build --base=${BASE:-origin/main} --head=${HEAD:-HEAD}", "typecheck:all": "yarn nx run-many -t typecheck", "typecheck:affected": "yarn nx affected -t typecheck --base=${BASE:-origin/main} --head=${HEAD:-HEAD}", "lint:all": "yarn nx run-many -t lint", "lint:affected": "yarn nx affected -t lint --base=${BASE:-origin/main} --head=${HEAD:-HEAD}", "quality:all": "yarn nx run-many --targets=typecheck,lint", "quality:affected": "yarn nx affected --targets=typecheck,lint --base=${BASE:-origin/main} --head=${HEAD:-HEAD}" } Tutaj definiujemy cztery typy skryptów wykonawczych: Buduje pakiet. build: Sprawdza typy pakietu. typecheck: Kłaczki na opakowaniu. lint: Uruchamia zarówno kontrolę typu, jak i lint. jakość: Każdy skrypt ma dwie wersje: Uruchamia skrypt dla wszystkich pakietów. wszystkie: Uruchamia skrypt tylko w przypadku pakietów, których dotyczą ostatnie zmiany. Zmienne środowiskowe i pozwalają określić zakres (domyślnie i bieżący ), umożliwiając szczegółowe wykonywanie żądań ściągnięcia. Może to znacznie zaoszczędzić czas i zasoby. affected: BASE HEAD origin/main HEAD Zarządzanie zależnościami cyklicznymi NX udostępnia również do generowania grafu zależności, który może pomóc w wykrywaniu cykli zależności. Poniższy skrypt używa wyjścia grafu NX do sprawdzania zależności cyklicznych i kończy się niepowodzeniem, jeśli takie zostaną znalezione. wbudowane polecenie Utwórz plik o nazwie z następującą zawartością: scripts/check-circulardeps.mjs import { execSync } from "child_process"; import path from "path"; import fs from "fs"; const hasCycle = (node, graph, visited, stack, path) => { if (!visited.has(node)) { visited.add(node); stack.add(node); path.push(node); const dependencies = graph.dependencies[node] || []; for (const dep of dependencies) { const depNode = dep.target; if ( !visited.has(depNode) && hasCycle(depNode, graph, visited, stack, path) ) { return true; } if (stack.has(depNode)) { path.push(depNode); return true; } } } stack.delete(node); path.pop(); return false; }; const getGraph = () => { const cwd = process.cwd(); const tempOutputFilePath = path.join(cwd, "nx-graph.json"); execSync(`nx graph --file=${tempOutputFilePath}`, { encoding: "utf-8", }); const output = fs.readFileSync(tempOutputFilePath, "utf-8"); fs.rmSync(tempOutputFilePath); return JSON.parse(output).graph; }; const checkCircularDeps = () => { const graph = getGraph(); const visited = new Set(); const stack = new Set(); for (const node of Object.keys(graph.dependencies)) { const path = []; if (hasCycle(node, graph, visited, stack, path)) { console.error("🔴 Circular dependency detected:", path.join(" → ")); process.exit(1); } } console.log("✅ No circular dependencies detected."); }; checkCircularDeps(); Ten skrypt: Wykonuje polecenie NX w celu wygenerowania grafu zależności. Odczytuje wykres z tymczasowego pliku JSON. Rekurencyjnie sprawdza cykle. Rejestruje błąd i wychodzi, jeśli zostanie wykryta zależność cykliczna. Sprawdzanie zależności za pomocą ograniczeń Yarn W miarę rozwoju projektów utrzymanie spójności między zależnościami staje się wyzwaniem. Egzekwowanie ścisłych reguł dotyczących zależności, wersji Node i innych konfiguracji jest niezbędne, aby uniknąć niepotrzebnego długu technicznego. Ograniczenia Yarn oferują sposób na automatyzację tych walidacji. Zrozumienie ograniczeń przędzy Yarn Constraints to zbiór reguł dla pakietów w Twoim monorepo. Istotną zaletą ich używania jest to, że jesteś menedżerem tych reguł. Na przykład możesz utworzyć regułę, aby wymusić na wszystkich pakietach używanie tej samej wersji React. Po jej ustawieniu nigdy nie napotkasz problemu, gdy aplikacja hosta nie może użyć funkcji/biblioteki z wyższą wersją React. Chociaż migracja dużego monorepo do nowej głównej wersji zależności może być skomplikowana, wykorzystanie ograniczeń ostatecznie zapewnia spójność i stabilność całego projektu. Wymuszanie spójności W naszym przykładowym repozytorium używamy pliku w celu wymuszenia spójności dla: yarn.config.cjs Wersja węzła Wersja włóczki Wersje zależności Aby zapewnić elastyczność podczas przejść, możesz zdefiniować wykluczenia, aby tymczasowo ominąć pewne kontrole. Na przykład: const workspaceCheckExclusions = []; const dependencyCheckExclusions = []; Stałe te umożliwiają wykluczenie określonych obszarów roboczych lub zależności z procesu walidacji, co zapewnia płynne migracje, gdy zajdzie taka potrzeba. Zarządzanie wersjami za pomocą zestawów zmian Innym problemem, z którym możesz się spotkać przy wzroście repozytorium, jest zarządzanie wersjami i wydawanie. Zestawy zmian zapewniają eleganckie rozwiązanie do automatyzacji tego procesu, zapewniając, że każda zmiana jest śledzona, wersjonowana i wydawana. Wprowadzenie do zestawów zmian to narzędzie typu open source przeznaczone do zarządzania wersjonowaniem w repozytoriach monorepo. Upraszcza proces śledzenia zmian, przydzielając je do małych, czytelnych dla człowieka dokumentów, które odzwierciedlają intencję zmiany. Dokumenty te nazywane są changesetami. Kluczowe korzyści obejmują: Changesets Przejrzysta dokumentacja Każdy zestaw zmian przedstawia wprowadzone zmiany, co pomaga zarówno deweloperom, jak i konsumentom zrozumieć, czego mogą się spodziewać po nowej wersji. Granularna kontrola wersji Każdy pakiet jest wersjonowany niezależnie, co zapewnia, że tylko pakiety, których to dotyczy, są aktualizowane. Minimalizuje to ryzyko pustych podbić wersji i zerwania zależności. Przyjazny dla współpracy Ponieważ każda zmiana jest rejestrowana w zestawie zmian, zespoły mogą przeglądać i zatwierdzać aktualizacje przed faktycznym wydaniem. Automatyzacja wydań Jedną z najpotężniejszych funkcji Changesets jest możliwość automatyzacji procesu. Możesz zintegrować Changesets z Twoim potokiem CI/CD i zapomnieć o ręcznych zmianach wersji i publikowaniu NPM. Spójrz na przepływ pracy w przykładowym repozytorium. Ma on krok . Krok wspierany przez GitHub action tworzy całą magię. Musisz tylko skonfigurować do publikowania swoich pakietów. Następnie każde wypchnięcie do gałęzi spowoduje: release.yaml create-release-pull-request-or-publish changesets/action NPM_TOKEN main . Sprawdź, czy istnieją jakieś dokumenty Changeset Jeśli dokumenty zestawu zmian są obecne, akcja tworzy żądanie ściągnięcia z niezbędnymi podbiciami wersji i aktualizacjami dziennika zmian. Jeśli nie zostaną wykryte żadne zmiany, nic się nie dzieje. . Sprawdź, czy są jakieś pakiety gotowe do opublikowania Jeśli pakiety są gotowe do wydania, akcja publikuje nowe wersje w NPM przy użyciu dostarczonego . Jeśli nie ma żadnych pakietów gotowych do opublikowania, akcja kończy działanie bez wprowadzania zmian. NPM_TOKEN Automatyzując te zadania, Changesets gwarantuje spójność i niezawodność wydań, zmniejszając ryzyko wystąpienia błędu ludzkiego i usprawniając proces tworzenia oprogramowania. Integracja przepływu pracy z akcjami GitHub Ta sekcja zagłębia się w to, jak uwolnić moc architektury, którą właśnie zbudowaliśmy. Używając GitHub Actions, zautomatyzujemy kontrole jakości PR, wydania wersji dla bibliotek i funkcji oraz wdrożenia aplikacji. Skupiamy się na maksymalizacji automatyzacji przy jednoczesnym zachowaniu jakości kodu i szczegółowości zadań. Sprawdź jakość PR Aby zapewnić spójność i stabilność kodu pull request, tworzymy dedykowany przepływ pracy . Ten przepływ pracy wykonuje kilka zadań, takich jak zapewnienie, że ręczne zmiany wersji nie zostaną wprowadzone (ponieważ wersjonowanie jest zarządzane przez Changesets): quality.yaml - id: check_version name: Check version changes run: | BASE_BRANCH=${{ github.event.pull_request.base.ref }} git fetch origin $BASE_BRANCH CHANGED_FILES=$(git diff --name-only origin/$BASE_BRANCH HEAD) VERSION_CHANGED=false for FILE in $CHANGED_FILES; do if [[ $FILE == */package.json ]]; then if [ -f "$FILE" ]; then HEAD_VERSION=$(grep '"version":' "$FILE" | awk -F '"' '{print $4}') else continue fi HEAD_VERSION=$(cat $FILE | grep '"version":' | awk -F '"' '{print $4}') if git cat-file -e origin/$BASE_BRANCH:$FILE 2>/dev/null; then BASE_VERSION=$(git show origin/$BASE_BRANCH:$FILE | grep '"version":' | awk -F '"' '{print $4}') else BASE_VERSION=$HEAD_VERSION fi if [ "$BASE_VERSION" != "$HEAD_VERSION" ]; then VERSION_CHANGED=true echo "Version change detected in $FILE" fi fi done if [ "$VERSION_CHANGED" = true ]; then echo "Manual version changes are prohibited. Use changesets instead." exit 1 fi env: GITHUB_REF: ${{ github.ref }} Oprócz tego sprawdzenia zadanie instaluje zależności, weryfikuje ograniczenia, sprawdza zależności cykliczne i weryfikuje ogólną jakość kodu przy użyciu skryptu, który zdefiniowaliśmy wcześniej w NX: check-quality - id: install-dependencies name: Install dependencies run: yarn --immutable - id: check-constraints name: Check constraints run: yarn constraints - id: check-circulardeps name: Check circular dependencies run: yarn check-circulardeps:all - id: check-quality name: Check quality run: BASE=origin/${{ github.event.pull_request.base.ref }} yarn quality:affected Kontrola jakości jest zaprojektowana tak, aby działała tylko na pakietach objętych bieżącym żądaniem ściągnięcia. Pomyślne ukończenie tych zadań sygnalizuje, że żądanie ściągnięcia jest gotowe do scalenia (oprócz otrzymania przeglądów kodu). Jeśli w Twoim projekcie konieczne będą dodatkowe kontrole, możesz zaktualizować plik i skrypt jakości, zachowując niezmieniony przepływ pracy. nx.json Publikuj biblioteki i funkcje Po scaleniu PR uruchamiany jest przepływ pracy wydania (opisany w rozdziale Changesets). Ten przepływ pracy buduje pakiety, których to dotyczy, i tworzy PR z podbiciem wersji. Po zatwierdzeniu i scaleniu tego PR uruchamia się ponownie — tym razem zamiast tworzyć PR, wykrywa zmiany wersji i wydaje zaktualizowane pakiety do NPM: release.yaml - id: build-packages name: Build packages run: yarn build:affected - id: create-release-pull-request-or-publish name: Create Release Pull Request or Publish to NPM uses: changesets/action@v1 with: version: yarn changeset version publish: yarn release commit: "chore: publish new release" title: "chore: publish new release" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} release-apps: needs: release-libs-features uses: ./.github/workflows/release-apps.yaml with: publishedPackages: ${{ needs.release-libs-features.outputs.publishedPackages }} Następnie wykonywane jest zadanie o nazwie , które odpowiada za wdrożenia aplikacji. Otrzymuje listę opublikowanych pakietów z poprzedniego kroku i przenosi nas do następnego rozdziału. release-apps Publikuj aplikacje Ostatnia część procesu wydania obejmuje wdrożenie aplikacji (aplikacje nie są publikowane w NPM, ponieważ są ustawione w ). Przepływ pracy jest automatycznie wyzwalany przez lub może być wykonywany bezpośrednio z zakładki Actions w GitHub: private package.json release-apps.yaml release.yaml name: Release Apps on: workflow_call: inputs: publishedPackages: description: "List of published packages" required: false type: string default: "[]" workflow_dispatch: inputs: publishedPackages: description: "List of published packages (optional)" required: false type: string default: "[]" Ten przepływ pracy akceptuje dane wejściowe , aby określić, które pakiety zostały opublikowane. Używając strategii macierzy, sprawdza każdą aplikację macierzy pod kątem obecności opublikowanych zależności: publishedPackages - id: check-dependency-published name: Check if any app dependency is published run: | PUBLISHED_PACKAGES="${{ inputs.publishedPackages }}" PACKAGE_NAME="${{ matrix.package }}" APP="${{ matrix.app }}" DEPENDENCIES=$(jq -r '.dependencies // {} | keys[]' "apps/$APP/package.json") for DEP in $DEPENDENCIES; do if echo "$PUBLISHED_PACKAGES" | grep -w "$DEP"; then echo "published=true" >> $GITHUB_OUTPUT exit 0 fi done echo "published=false" >> $GITHUB_OUTPUT To sprawdzenie jest jednym z warunków inicjowania wdrożenia aplikacji. Drugi warunek zapewnia, że wersja aplikacji została zmieniona (co oznacza, że ponowne wdrożenie jest konieczne, nawet jeśli nie zaktualizowano żadnych zależności): - id: check-version-change name: Check if app version has changed run: | APP="${{ matrix.app }}" PACKAGE_JSON_PATH="apps/$APP/package.json" CURRENT_VERSION=$(jq -r '.version' "$PACKAGE_JSON_PATH") PREVIOUS_VERSION=$(git show HEAD~1:"$PACKAGE_JSON_PATH" | jq -r '.version' || echo "") if [[ "$CURRENT_VERSION" == "$PREVIOUS_VERSION" ]]; then echo "changed=false" >> $GITHUB_OUTPUT else echo "changed=true" >> $GITHUB_OUTPUT fi Na koniec, po potwierdzeniu, że aplikacja ma zaktualizowane zależności lub że jej wersja uległa zmianie, przepływ pracy pobiera nową wersję i przystępuje do kompilowania i wdrażania aplikacji: - id: set-up-docker name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - id: get-app-version name: Get the app version from package.json run: echo "app-version=$(cat ./apps/${{ matrix.app }}/package.json | jq -r '.version')" >> $GITHUB_OUTPUT - id: build-image name: Build image if: steps.check-dependency-published.outputs.published == 'true' || steps.check-version-change.outputs.changed == 'true' uses: docker/build-push-action@v4 with: build-contexts: | workspace=./ context: "./apps/${{ matrix.app }}" load: true push: false tags: | ${{ matrix.app }}:v${{ steps.get-app-version.outputs.app-version }} W tym przykładzie budujemy obraz Dockera bez wypychania go do rejestru. W swoim przepływie pracy produkcyjnej zastąp ten krok rzeczywistym procesem wdrażania. Wniosek Podsumowanie najlepszych praktyk W tym artykule przyjrzeliśmy się konfiguracji solidnego monorepo i narzędziom, które pomagają sprawnie nim zarządzać. Centralizując bazę kodu, nie tylko upraszczasz zarządzanie zależnościami, ale także usprawniasz współpracę między zespołami. Pokazaliśmy, jak Yarn może być wykorzystywany do współdzielenia zależności, przyspieszania instalacji z PnP i poprawy ogólnej spójności projektu. Ponadto integracja NX w celu ukierunkowanego wykonywania skryptów zapewnia, że CI jest szybkie i wydajne. Zestawy zmian pomogły zautomatyzować wersjonowanie, zmniejszając liczbę błędów ręcznych i usprawniając wydania. Na koniec stworzyliśmy gotowy do produkcji potok CI/CD z akcjami GitHub, który wykonuje tylko niezbędne zadania. Następne kroki : Zacznij od skonfigurowania monorepo na małą skalę, aby przetestować te najlepsze praktyki. Eksperymentuj z różnymi strukturami folderów i stopniowo rozszerzaj je, aby uwzględnić więcej pakietów, gdy wzrośnie Twoje zaufanie. Eksperymentuj i dostosowuj : Rozważ zintegrowanie uzupełniających narzędzi, takich jak PNPM lub Turborepo, w zależności od unikalnych wymagań Twojego projektu i preferencji zespołu. Zintegruj dodatkowe narzędzia : dostosuj przepływy pracy GitHub Actions, aby uwzględnić dodatkowe kontrole jakości, pokrycie kodu i skanowanie zabezpieczeń dostosowane do Twojego projektu. Ulepsz procesy CI/CD : Bądź na bieżąco z najnowszymi wersjami Yarn, NX i Changesets. Współpracuj ze społecznością, aby dzielić się spostrzeżeniami i poznawać pojawiające się trendy w zarządzaniu monorepo. Społeczność i aktualizacje Zasoby : Przykładowe repozytorium Uzyskaj dostęp do kompletnego repozytorium przykładów utworzonego na potrzeby tego przewodnika. Poznaj strukturę projektu, przykłady kodu i skrypty, które pokazują konfigurację monorepo w działaniu. : Opublikowane pakiety NPM Sprawdź rzeczywisty pakiet NPM opublikowany w ramach tego projektu. Te pakiety demonstrują rzeczywiste wykorzystanie i implementację koncepcji omówionych w artykule.