Development Setup

Code Style

Code conventions, ESLint rules, and formatting standards for Cognova.

Code Style

Cognova uses ESLint with the @nuxt/eslint module for consistent formatting. The config is in eslint.config.mjs and nuxt.config.ts.

Core Rules

No semicolons

// Good
const name = 'Cognova'
export default defineEventHandler(async (event) => { ... })

// Bad
const name = 'Cognova';

No brackets for single-line control flow

// Good
if (!body.title)
  throw createError({ statusCode: 400, message: 'title is required' })

for (const item of items)
  processItem(item)

// Bad
if (!body.title) {
  throw createError({ statusCode: 400, message: 'title is required' })
}

Comma dangle: never

// Good
const config = {
  ssl: false,
  max: 10
}

// Bad
const config = {
  ssl: false,
  max: 10,
}

Brace style: 1tbs

// Good
if (condition) {
  doSomething()
} else {
  doOther()
}

// Bad (Allman style)
if (condition)
{
  doSomething()
}

Operator linebreak

Operators go at the beginning of the next line, not the end of the current line:

// Good
const result
  = someValue
    + otherValue

// Bad
const result =
  someValue +
  otherValue

Union type format

Union types use a leading | on each line:

// Good
export type Status
  = 'todo'
  | 'in_progress'
  | 'done'
  | 'blocked'

// Bad
export type Status = 'todo' | 'in_progress' | 'done' | 'blocked'

Vue Conventions

Composition API only

All components use <script setup lang="ts">. No Options API.

<script setup lang="ts">
const props = defineProps<{ title: string }>()
const count = ref(0)
</script>

Single root element

Every template must have a single root element:

<!-- Good -->
<template>
  <div>
    <h1>Title</h1>
    <p>Content</p>
  </div>
</template>

<!-- Bad -->
<template>
  <h1>Title</h1>
  <p>Content</p>
</template>

Multi-panel pages

Pages with resizable panels wrap in a flex container:

<template>
  <div class="flex flex-1 min-w-0">
    <UDashboardPanel id="sidebar" :default-size="25" resizable>
      <!-- sidebar content -->
    </UDashboardPanel>
    <UDashboardPanel id="main" grow>
      <!-- main content -->
    </UDashboardPanel>
  </div>
</template>

Import Conventions

Frontend (app/)

Use the ~/ alias which resolves to app/:

import { useAuth } from '~/composables/auth'

Most imports are auto-imported by Nuxt (Vue reactivity, composables, components).

Server (server/)

Use the ~~/server/ alias for all server-internal imports:

// Good
import { getDb } from '~~/server/db'
import { validatePath } from '~~/server/utils/path-validator'

// Bad -- relative paths break when files move
import { getDb } from '../db'
import { validatePath } from '../../utils/path-validator'

Shared types

Types shared between frontend and backend live in shared/types/:

import type { Task, FileEntry, NotificationPayload } from '~~/shared/types'

Never duplicate type definitions. If both app/ and server/ need a type, it belongs in shared/types/.

File Naming

LocationConventionExample
API routesresource.method.tstasks/index.get.ts, [id]/index.put.ts
Vue componentsPascalCaseTaskCard.vue, SearchModal.vue
Nested componentsDirectory + nameHome/Header.vue (used as <HomeHeader />)
ComposablescamelCase with use prefixuseSearch.ts, useAuth.ts
Server utilskebab-casepath-validator.ts, notification-bus.ts
Server pluginsNumber prefix for ordering00.env-validate.ts, 02.database.ts

Running the Linter

# Lint the entire project
pnpm lint

# With auto-fix
pnpm lint --fix

The docs/ directory is excluded from linting via the ESLint config.