Недавно работая над GraphQL-проектом, у меня была задача: схему (часть Infrastructure-as-Code) нужно было экспортировать в TypeScript для фронтенда и бэкенда. Я быстро понял, что поскольку схема эволюционирует по мере добавления фич, я трачу время на а) ручные действия и б) сравнение версий типов между схемой, фронтом и бэком — что могло приводить к ошибкам. Поэтому придумал быстрое и простое решение.

Для начала генерируем типы из схемы. Используем graphql-codegen — устанавливаем в репозитории со схемой GraphQL:

npm -i @graphql-codegen/typescript @graphql-codegen/cli --save-dev

И настраиваем через codegen.ts в корне того же репозитория:

import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  overwrite: true,
  schema: "path/to/your/schema.graphql",
  generates: {
    "graphql-types.ts": {
      plugins: ["typescript"],
    },
  },
};

export default config;

Проверяем, что работает: graphql-codegen --config codegen.ts. Затем создаем bash-скрипт generate-types.sh, который создает локальный npm-пакет:

#!/bin/bash

# Usage: ./generate-types.sh <version>
# Example: ./generate-types.sh 1.2.3

# Check if a version argument is provided
if [ -z "$1" ]; then
  echo "Error: Version not specified."
  echo "Usage: ./generate-types.sh <version>"
  exit 1
fi

VERSION=$1

# Configuration variables
PACKAGE_NAME="@my-project/graphql-types"
FILENAME="graphql-types.ts"

# Generate graphql-types.ts
echo "Generating $FILENAME..."
graphql-codegen --config codegen.ts

# Prepare a temporary directory for publishing
echo "Preparing package $PACKAGE_NAME for version $VERSION..."
mkdir -p ./dist
mv ./$FILENAME ./dist/index.d.ts

# Create a minimal package.json in the dist folder with the specified version
echo "{
  \"name\": \"$PACKAGE_NAME\",
  \"version\": \"$VERSION\",
  \"types\": \"index.d.ts\"
}" > ./dist/package.json

# Create the package
echo "Packaging $PACKAGE_NAME version $VERSION..."
npm pack ./dist

# Clean up
echo "Cleaning up..."
rm -rf ./dist

echo "$PACKAGE_NAME@$VERSION generated successfully!"

Этот скрипт я добавил в инфраструктурный репозиторий. Можно было бы завести отдельный package.json — и это правильно — но я предпочитаю держать все связанное в одном месте. Вызов скрипта с версией создаст пакет этой версии. Я предпочитаю коммитить этот пакет с номером версии.

Теперь импортируем его во фронтенд или бэкенд. Для этого я тоже написал скрипт:

#!/bin/bash

# Usage: ./import-types.sh <version>
# Example: ./import-types.sh 1.2.3

# Define the paths to the repositories
EXPORTER_DIR="./../my-project"
IMPORTER_DIR="."

# Check if a version argument is provided
if [ -z "$1" ]; then
  echo "Error: Version not specified."
  echo "Usage: ./import-package.sh <version>"
  exit 1
fi

# Set the package name with the provided version
PACKAGE_NAME="my-project-graphql-types"
PACKAGE_VERSION="$1"
PACKAGE_FILE="$PACKAGE_NAME-$PACKAGE_VERSION.tgz"

# Check if the exporter directory exists
if [ ! -d "$EXPORTER_DIR" ]; then
    echo "Error: Exporter folder $EXPORTER_DIR not found."    exit 1
fi

# Check if the package file exists
if [ ! -f "$EXPORTER_DIR/$PACKAGE_FILE" ]; then
    echo "Error: Package $PACKAGE_FILE not found in $EXPORTER_DIR."
    exit 1
fi

# Navigate to the importer directory
echo "Navigating to $IMPORTER_DIR..."
cd "$IMPORTER_DIR" || { echo "Error: Could not navigate to $IMPORTER_DIR."; exit 1; }

echo "Cleaning existing versions of $PACKAGE_NAME..."
rm -f "${PACKAGE_NAME}-"*.tgz

# Copy package to importer directory
echo "Copying $PACKAGE_FILE to $IMPORTER_DIR..."
cp "$EXPORTER_DIR/$PACKAGE_FILE" . || { echo "Error: Could not copy $PACKAGE_FILE to $IMPORTER_DIR."; exit 1; }

# Install the package from the exporter repository
echo "Installing package $PACKAGE_FILE..."
npm install "$PACKAGE_FILE"

# Check if the installation was successful
if [ $? -eq 0 ]; then
    echo "Package $PACKAGE_FILE installed successfully in $IMPORTER_DIR!"
else
  echo "Error: Failed to install package $PACKAGE_FILE."
  exit 1
fi

Этот скрипт коммитим в репозитории фронтенда и бэкенда и запускаем так же — с версией в аргументе. Теперь IDE подхватит типы:

import { MyType } from "@my-project/graphql-types";

function somewhereInFrontend(obj: MyType) {}

Очевидно, процесс можно улучшить — выбирать последнюю версию в репозитории со схемой и добавить нормальную публикацию через npm (тогда второй скрипт не понадобится). Но мой рабочий процесс уже улучшился:

  • одна команда в инфраструктурном репозитории
  • одна команда в репозитории фронтенда и бэкенда
  • автоматическая генерация типов
  • версионирование

Надеюсь, поможет и улучшит ваш процесс тоже.