Skip to content

Current structure is not tree-shakeable and has a lot of duplicate code #3

@roman-psc

Description

@roman-psc

Current structure is not tree-shakeable and has a lot of duplicate code.

Proposed solutions:

1. Tree-shaking:

### 1\. The Architecture Problem

#### Current: The Monolith (Not Tree-Shakeable)

You likely generate a class that looks like this:

```typescript
// dist/index.mjs
import { UserSchema, GameSchema, ... } from './schemas'; // Imports ALL 180KB of Schemas

export class LichessClient {
  constructor(token) { ... }

  // These are all bundled together. You can't separate them.
  getUser() { ... }       // Uses UserSchema
  getGame() { ... }       // Uses GameSchema
  getTournament() { ... } // Uses TournamentSchema
  // ... + 100 other methods
}
```

**Result:** The user imports `LichessClient`, and the bundler sees that `LichessClient` depends on *every single schema*. The whole 300KB+ blob gets included.

#### Target: Functional Composition (Tree-Shakeable)

This is the pattern used by **Firebase v9+**, **Supabase**, and **Octokit**. Instead of a class with methods, you have a lightweight "client" (that just holds state) and standalone functions.

```typescript
// dist/client.mjs
// 1. The Client is just state (URL, token). 0KB logic.
export const createClient = (token) => ({ token, baseUrl: '...' });

// dist/users.mjs
// 2. Functions are standalone. 
// This file ONLY imports UserSchema. It does not know about Games or Tournaments.
import { UserSchema } from './schemas/users'; 

export const getUser = async (client, id) => {
  const res = await fetch(`${client.baseUrl}/user/${id}`, ...);
  return UserSchema.parse(res);
}
```

**Result:** If the user only imports `getUser`, the bundler **only** includes the code for `getUser` and `UserSchema`. The `GameSchema` and `TournamentSchema` are never touched and are dropped from the bundle.

-----

### 2\. How to Refactor Your Generator

Since you are using a script (`gen:client`) to build this, you don't need to rewrite code manually. You need to change the template your generator uses.

#### Phase 1: Update `scripts/yaml-to-client`

Instead of generating one big class string, generate many small exports.

**Change this output structure:**

```typescript
export class LichessClient {
  async getUser(id: string) { ... }
  async getGame(id: string) { ... }
}
```

**To this output structure:**

```typescript
// src/client.ts (The minimal core)
export interface LichessContext {
  token?: string;
  baseUrl?: string;
  fetch?: typeof fetch;
}

// src/endpoints/users.ts (Generated separately or as named exports)
import type { LichessContext } from '../client';
import { UserSchema } from '../schemas'; // Ideally import only specific schema

export const getUser = (ctx: LichessContext, id: string) => {
   // implementation
}
```

### 3\. Addressing the "Types are 300KB" Issue

You mentioned your client types are 300KB. This suggests **Type Inlining**.

If your generated code looks like this:

```typescript
// generated
export function getUser(id: string): { id: string, username: string, title?: string ... } { ... }
```

TypeScript is repeating the entire User object structure in the `.d.ts` file for every function that uses it.

**The Fix:**
Ensure your generator outputs code that references the *Interface* by name, not value.

```typescript
import type { User } from '../schemas'; // Import the type definition

// generated
export function getUser(id: string): User { ... }
```

This makes your `.d.ts` file tiny because it just points to the definition in `schemas.d.ts` rather than rewriting it.

### 4\. A "Hybrid" Approach (for DX)

If you hate the functional syntax (`getUser(client, 'id')`) and prefer the dot notation (`client.users.get('id')`), you can still achieve tree-shaking by using **namespaced exports** or **sub-path exports**, but it is harder.

The functional approach (Option 2 above) is the gold standard for package size.

**Example usage of the new Functional approach:**

```typescript
import { createClient } from '@lichess/api'; 
import { getUser } from '@lichess/api/users'; // Subpath import

const client = createClient({ token: '...' });

// Only bundles code for getUser and the User Schema
const user = await getUser(client, 'gamerman');
```

### Summary of Next Steps

1.  **Modify `yaml-to-client`**: Stop generating a `class`. Start generating standalone functions that accept a `client` object as the first argument.
2.  **Check Imports**: Ensure each generated function file only imports the *specific* Zod schema it needs, not a barrel file (`index.ts`) that exports *all* schemas.
3.  **Fix Type References**: Ensure the return types reference the named interfaces from `@lichess/api/schemas` to reduce `.d.ts` bloat.

Generated by Google Gemini

Metadata

Metadata

Assignees

Labels

help wantedExtra attention is needed

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions