@villads_unicorn_leth/uniscript
v0.1.0
Published
UniScript - Opinionated TypeScript linting for scalable codebases. Enforces strict architectural patterns for LLM-friendly development.
Maintainers
Readme
UniScript 🦄
Opinionated TypeScript linting for scalable codebases - enforces strict architectural patterns for LLM-friendly development.
Like Ruby on Rails for TypeScript - strict, opinionated, and designed for AI-assisted coding.
Philosophy
UniScript enforces strict, opinionated patterns that make your codebase predictable, maintainable, and AI-friendly. By following these patterns, both humans and LLMs can navigate and modify your code with confidence.
Installation
npm install uniscript --save-devQuick Start
Add to your eslint.config.js (ESLint 9+ flat config):
import uniscript from 'uniscript'
export default [
// Your other configs...
uniscript.configs.recommended,
]Rules
uniscript/require-backend-wrapper
Error: All exported functions in /backend/**/route.ts files must use backendWrapper.
// ❌ Bad - missing backendWrapper
export async function getUsers(supabase: TypedSupabaseClient) {
const { data, error } = await supabase.from('users').select('*')
if (error) throw error
return data
}
// ✅ Good - uses backendWrapper
export async function getUsers(
supabase: TypedSupabaseClient
): Promise<FunctionResult<User[]>> {
return backendWrapper({
ctx: { supabase },
input: {},
outputSchema: usersSchema,
fn: async ({ supabase }) => {
const { data, error } = await supabase.from('users').select('*')
if (error) {
return { data: null, error, message: 'Failed to fetch users' }
}
return { data, error: null, message: 'Users fetched successfully' }
},
})
}uniscript/require-api-wrapper
Error: All HTTP method exports (GET, POST, PUT, PATCH, DELETE) in /app/api/**/route.ts must use apiWrapper.
// ❌ Bad - raw handler
export async function GET(request: NextRequest) {
// Manual auth, validation, error handling...
}
// ✅ Good - uses apiWrapper
export const GET = apiWrapper(
async (request, { query, supabaseClient, teamId }) => {
const { data, error } = await getUsers(supabaseClient, teamId)
if (error) {
return NextResponse.json({ data: null, error, message }, { status: 500 })
}
return NextResponse.json({ data, error: null, message })
},
{
bodySchema: emptyZodObject,
querySchema: getUsersQuerySchema,
requiredScope: 'read:users',
}
)uniscript/no-try-catch-in-backend
Error: No try-catch blocks in backend route files. The backendWrapper handles all error catching.
// ❌ Bad - manual try-catch
export async function getUser(supabase, id) {
return backendWrapper({
fn: async ({ supabase }) => {
try {
const { data, error } = await supabase.from('users').select('*').eq('id', id).single()
if (error) throw error
return { data, error: null, message: 'Success' }
} catch (err) {
return { data: null, error: err, message: 'Failed' }
}
},
})
}
// ✅ Good - let backendWrapper handle errors
export async function getUser(supabase, id) {
return backendWrapper({
fn: async ({ supabase }) => {
const { data, error } = await supabase.from('users').select('*').eq('id', id).single()
if (error) {
return { data: null, error, message: 'Failed to fetch user' }
}
return { data, error: null, message: 'User fetched successfully' }
},
})
}uniscript/require-function-result-type
Error: Exported backend functions must have explicit Promise<FunctionResult<T>> return type.
// ❌ Bad - missing return type
export async function getUsers(supabase: TypedSupabaseClient) {
// ...
}
// ✅ Good - explicit return type
export async function getUsers(
supabase: TypedSupabaseClient
): Promise<FunctionResult<User[]>> {
// ...
}uniscript/no-zod-in-routes
Error: Don't import zod directly in route files. Define schemas in schema.ts.
// ❌ Bad - zod import in route.ts
import { z } from 'zod'
const userSchema = z.object({ name: z.string() })
// ✅ Good - import from schema.ts
import { userSchema, type User } from './schema'Configurations
recommended
Enables all rules as errors. Best for new projects.
import uniscript from 'uniscript'
export default [uniscript.configs.recommended]backend
Only backend-related rules (no API wrapper rule).
export default [uniscript.configs.backend]api
Only API route rules.
export default [uniscript.configs.api]Customizing Rules
Override default patterns in your config:
import uniscript from 'uniscript'
export default [
{
plugins: {
uniscript,
},
rules: {
'uniscript/require-backend-wrapper': ['error', {
backendPatterns: ['**/backend/**', '**/server/**'],
wrapperName: 'backendWrapper',
filePatterns: ['**/route.ts'],
}],
'uniscript/require-api-wrapper': ['error', {
apiPatterns: ['**/app/api/**'],
wrapperName: 'apiWrapper',
httpMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
}],
},
},
]Why UniScript?
🤖 LLM-Friendly
Consistent patterns mean AI assistants can:
- Understand your codebase structure instantly
- Generate correct code following your conventions
- Make changes without breaking patterns
🏗️ Scalable Architecture
- Centralized error handling via wrappers
- Consistent response format (
{ data, error, message }) - Schema validation in dedicated files
- Type safety enforced at boundaries
🔍 IDE Integration
All rules show as errors on hover in your IDE (VSCode, Cursor, etc.), making violations immediately visible.
Contributing
- Fork the repo
- Create a feature branch
- Add tests for new rules
- Submit a PR
License
MIT © Villads Leth
