Skip to main content

Overview

Gitea’s frontend is built with Vue.js 3, TypeScript, and Webpack, combining server-rendered templates with reactive components.

Technology Stack

Framework

Vue.js 3.5+
  • Composition API
  • Single-file components
  • Reactive state management

Language

TypeScript
  • Type-safe JavaScript
  • Better IDE support
  • Fewer runtime errors

Build Tool

Webpack 5
  • Module bundling
  • Asset optimization
  • Code splitting

Package Manager

pnpm 10+
  • Fast, efficient
  • Monorepo support
  • Disk space savings

Directory Structure

web_src/
├── js/
│   ├── components/      # Vue components
│   │   ├── RepoActionView.vue
│   │   ├── DiffFileTree.vue
│   │   └── ...
│   ├── features/        # Feature-specific code
│   │   ├── repo-issue.ts
│   │   ├── repo-diff.ts
│   │   └── ...
│   ├── modules/         # Utility modules
│   │   ├── fetch.ts
│   │   ├── toast.ts
│   │   └── ...
│   └── index.ts         # Entry point
├── css/
│   ├── base.css
│   ├── repo.css
│   └── ...
└── svg/                 # SVG icons

Build System

Development Build

# Install dependencies
pnpm install

# Build frontend (one-time)
make frontend

# Watch mode (auto-rebuild on changes)
make watch-frontend

Production Build

# Optimized production build
NODE_ENV=production make frontend

Build Configuration

Webpack configuration in webpack.config.ts:
export default {
  entry: {
    index: resolve('web_src/js/index.ts'),
  },
  output: {
    path: resolve('public/assets'),
    filename: 'js/[name].[contenthash:8].js',
    chunkFilename: 'js/[name].[contenthash:8].js',
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
      },
      {
        test: /\.ts$/,
        use: 'esbuild-loader',
      },
    ],
  },
};

Vue Components

Component Structure

<script setup lang="ts">
import {ref, computed} from 'vue';

interface Props {
  repoId: number;
  issueId: number;
}

const props = defineProps<Props>();
const emit = defineEmits<{
  update: [id: number];
}>();

const isLoading = ref(false);
const data = ref([]);

const filteredData = computed(() => {
  return data.value.filter(item => item.visible);
});

const loadData = async () => {
  isLoading.value = true;
  try {
    const response = await fetch(`/api/v1/repos/${props.repoId}/issues/${props.issueId}`);
    data.value = await response.json();
  } finally {
    isLoading.value = false;
  }
};
</script>

<template>
  <div class="issue-view">
    <div v-if="isLoading" class="loading">Loading...</div>
    <div v-else>
      <div v-for="item in filteredData" :key="item.id">
        {{ item.title }}
      </div>
    </div>
  </div>
</template>

<style scoped>
.issue-view {
  padding: 1rem;
}

.loading {
  text-align: center;
  color: var(--color-text-light);
}
</style>

Registering Components

// web_src/js/index.ts
import {createApp} from 'vue';
import RepoActionView from './components/RepoActionView.vue';

// Mount Vue components
for (const el of document.querySelectorAll('.vue-repo-action-view')) {
  const data = el.getAttribute('data-props');
  const props = data ? JSON.parse(data) : {};
  
  createApp(RepoActionView, props).mount(el);
}

Template Integration

<!-- templates/repo/actions/view.tmpl -->
<div class="vue-repo-action-view" 
     data-props='{{JsonUtils.EncodeToString .Props}}'>
</div>

Styling

CSS Architecture

Gitea uses a combination of:
  • Base styles: Core styling and resets
  • Component styles: Scoped to Vue components
  • Utility classes: Tailwind-inspired utilities
  • CSS variables: For theming

CSS Variables

:root {
  --color-primary: #74ac54;
  --color-text: #0b132a;
  --color-background: #ffffff;
  --spacing-unit: 8px;
  --border-radius: 4px;
}

[data-theme="dark"] {
  --color-text: #e6e9ef;
  --color-background: #1a1a1a;
}

Responsive Design

/* Mobile first */
.container {
  padding: 1rem;
}

/* Tablet and up */
@media (min-width: 768px) {
  .container {
    padding: 2rem;
  }
}

/* Desktop */
@media (min-width: 1024px) {
  .container {
    max-width: 1200px;
    margin: 0 auto;
  }
}

TypeScript

Type Definitions

// web_src/js/types.ts
export interface Repository {
  id: number;
  owner: string;
  name: string;
  full_name: string;
  private: boolean;
  html_url: string;
}

export interface Issue {
  id: number;
  number: number;
  title: string;
  state: 'open' | 'closed';
  user: User;
  created_at: string;
  updated_at: string;
}

export interface User {
  id: number;
  login: string;
  avatar_url: string;
}

API Client

// web_src/js/modules/fetch.ts
import type {Repository, Issue} from '../types';

export async function fetchRepo(owner: string, repo: string): Promise<Repository> {
  const response = await fetch(`/api/v1/repos/${owner}/${repo}`);
  if (!response.ok) {
    throw new Error(`Failed to fetch repository: ${response.statusText}`);
  }
  return response.json();
}

export async function fetchIssues(owner: string, repo: string): Promise<Issue[]> {
  const response = await fetch(`/api/v1/repos/${owner}/${repo}/issues`);
  if (!response.ok) {
    throw new Error(`Failed to fetch issues: ${response.statusText}`);
  }
  return response.json();
}

State Management

Reactive Store

// web_src/js/stores/repo.ts
import {reactive, readonly} from 'vue';
import type {Repository} from '../types';

interface RepoState {
  current: Repository | null;
  isLoading: boolean;
}

const state = reactive<RepoState>({
  current: null,
  isLoading: false,
});

export function useRepoStore() {
  const setRepo = (repo: Repository) => {
    state.current = repo;
  };
  
  const loadRepo = async (owner: string, name: string) => {
    state.isLoading = true;
    try {
      const repo = await fetchRepo(owner, name);
      state.current = repo;
    } finally {
      state.isLoading = false;
    }
  };
  
  return {
    state: readonly(state),
    setRepo,
    loadRepo,
  };
}

Testing

Unit Tests

// web_src/js/components/RepoCard.test.ts
import {describe, it, expect} from 'vitest';
import {mount} from '@vue/test-utils';
import RepoCard from './RepoCard.vue';

describe('RepoCard', () => {
  it('renders repository name', () => {
    const wrapper = mount(RepoCard, {
      props: {
        repo: {
          id: 1,
          name: 'gitea',
          owner: 'go-gitea',
        },
      },
    });
    
    expect(wrapper.text()).toContain('gitea');
  });
});

E2E Tests

Playwright tests in tests/e2e/:
import {test, expect} from '@playwright/test';

test('create repository', async ({page}) => {
  await page.goto('/repo/create');
  await page.fill('input[name="repo_name"]', 'test-repo');
  await page.click('button[type="submit"]');
  
  await expect(page).toHaveURL(/\/.*\/test-repo/);
});

Performance Optimization

Code Splitting

// Lazy load large components
const MarkdownEditor = () => import('./components/MarkdownEditor.vue');

Asset Optimization

  • Image optimization: WebP format with fallbacks
  • Icon sprites: SVG sprite sheets
  • CSS minification: Production builds
  • Tree shaking: Remove unused code

Best Practices

Use TypeScript

Always type your components and functions

Scoped Styles

Use <style scoped> to prevent style leakage

Composition API

Prefer Composition API over Options API

Accessibility

Use semantic HTML and ARIA attributes

See Also

Architecture

Overall architecture overview

Building

Build Gitea from source

Contributing

Contribution guidelines

Build docs developers (and LLMs) love