Code Style
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
| Location | Convention | Example |
|---|---|---|
| API routes | resource.method.ts | tasks/index.get.ts, [id]/index.put.ts |
| Vue components | PascalCase | TaskCard.vue, SearchModal.vue |
| Nested components | Directory + name | Home/Header.vue (used as <HomeHeader />) |
| Composables | camelCase with use prefix | useSearch.ts, useAuth.ts |
| Server utils | kebab-case | path-validator.ts, notification-bus.ts |
| Server plugins | Number prefix for ordering | 00.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.