# Best Practices for Organizing Cordis Plugins in a Monorepo

> Organize Cordis plugins in your monorepo effectively using Yarn workspaces, scoped packages, and TypeScript path mapping for efficient development and composition.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: best-practices
- Published: 2026-09-12

---

**Monorepo organization in Cordis relies on Yarn workspaces, scoped package names under `@cordisjs/plugin-<name>`, and TypeScript path mapping to keep plugins isolated yet composable.**

The `cordiverse/cordis` repository demonstrates a battle-tested approach to managing multiple plugins within a single codebase. By following the architectural patterns established in the core framework, developers can maintain independent versioning, enable cross-package imports without relative paths, and ensure clean runtime composition. This guide covers the specific conventions used in the Cordis source code, from directory layout to dependency management.

## Package Structure and Workspace Configuration

Each Cordis plugin resides in its own package under the `packages/` directory. This layout mirrors the structure seen in `packages/loader/`, `packages/group/`, and `packages/hmr/`.

**Directory layout:** Create a dedicated folder for every plugin containing its own [`package.json`](https://github.com/cordiverse/cordis/blob/main/package.json), [`tsconfig.json`](https://github.com/cordiverse/cordis/blob/main/tsconfig.json), and source files. This encapsulation allows independent versioning and targeted publishing to npm.

**Naming convention:** Use the scoped format `@cordisjs/plugin-<name>` in the `name` field of [`package.json`](https://github.com/cordiverse/cordis/blob/main/package.json). As seen in [[`packages/loader/package.json`](https://github.com/cordiverse/cordis/blob/main/packages/loader/package.json)](https://github.com/cordiverse/cordis/blob/main/packages/loader/package.json), this prevents naming collisions and signals first-party status.

**Workspace integration:** Declare all plugin folders in the root [`package.json`](https://github.com/cordiverse/cordis/blob/main/package.json) workspaces array. Cordis uses Yarn workspaces to resolve intra-repo imports without registry round-trips, as defined in the root [[`package.json`](https://github.com/cordiverse/cordis/blob/main/package.json)](https://github.com/cordiverse/cordis/blob/main/package.json).

## TypeScript Path Mapping for Clean Imports

To enable seamless imports like `import { Loader } from '@cordisjs/plugin-loader'` without relative paths, Cordis configures a wildcard path alias in the root [[`tsconfig.json`](https://github.com/cordiverse/cordis/blob/main/tsconfig.json)](https://github.com/cordiverse/cordis/blob/main/tsconfig.json):

```json
{
  "compilerOptions": {
    "paths": {
      "@cordisjs/plugin-*": ["./packages/*/src"]
    }
  }
}

```

This mapping directs TypeScript to resolve any `@cordisjs/plugin-*` import to the corresponding `packages/*/src` directory. The configuration supports IDE auto-completion and ensures that imports mimic the public npm package structure during development.

## Plugin Entry Points and Export Patterns

Every plugin must expose a clean public API through a single entry point while avoiding side effects in the module initialization phase.

**Entry file:** Export all public functionality from [`src/index.ts`](https://github.com/cordiverse/cordis/blob/main/src/index.ts). The loader plugin in [[`packages/loader/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts)](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts) demonstrates this pattern by exporting the `Loader` class and related types without invoking `ctx.plugin` at the top level.

**Side-effect free exports:** Keep plugin registration logic out of the entry file. Expose pure functions or classes that accept a `Context` instance, allowing the consuming application or loader to control activation timing.

Example plugin structure:

```typescript
// packages/my-plugin/src/index.ts
import type { Context } from '@cordisjs/core'

export function myPlugin(ctx: Context) {
  ctx.logger?.('my-plugin').info('Plugin loaded')
  return {
    doSomething() {
      ctx.logger?.('my-plugin').info('Doing something')
    }
  }
}

```

## Runtime Loading and Composition

Cordis provides specialized plugins for dynamic loading and grouping, enabling flexible runtime architecture.

**Dynamic loading:** The `Loader` plugin in [[`packages/loader/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts)](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts) resolves and registers plugins by name at runtime. Configure it with a `baseUrl` pointing to your plugin directory:

```typescript
import { Cordis } from '@cordisjs/core'
import Loader from '@cordisjs/plugin-loader'

const app = new Cordis()
await app.plugin(Loader, { baseUrl: import.meta.url })
await app.loader.import('@cordisjs/plugin-group')

```

**Plugin grouping:** The `group` utility exported from [[`packages/group/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/group/src/index.ts)](https://github.com/cordiverse/cordis/blob/main/packages/group/src/index.ts) combines multiple plugins into a single logical unit:

```typescript
import { group } from '@cordisjs/plugin-group'

await ctx.plugin(
  group([
    '@cordisjs/plugin-loader',
    '@cordisjs/plugin-hmr'
  ])
)

```

## Testing and Documentation Standards

Maintain quality and discoverability by colocating tests and documentation with each plugin.

**Test isolation:** Place tests in a `tests/` subfolder within the plugin directory, as shown in [[`packages/loader/tests/index.spec.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/tests/index.spec.ts)](https://github.com/cordiverse/cordis/blob/main/packages/loader/tests/index.spec.ts). Configure the monorepo's `vitest` setup to run these tests, allowing imports via package names rather than relative paths.

**Documentation:** Include a [`README.md`](https://github.com/cordiverse/cordis/blob/main/README.md) in each plugin folder describing purpose, usage, and configuration options. The loader plugin's [[`README.md`](https://github.com/cordiverse/cordis/blob/main/README.md)](https://github.com/cordiverse/cordis/blob/main/packages/loader/README.md) serves as the reference for consuming the package.

## Dependency Management with Peer Dependencies

Prevent runtime duplication of the core framework by declaring `@cordisjs/core` as a peer dependency rather than a direct dependency.

**Peer dependency pattern:** In [[`packages/loader/package.json`](https://github.com/cordiverse/cordis/blob/main/packages/loader/package.json)](https://github.com/cordiverse/cordis/blob/main/packages/loader/package.json), `@cordisjs/core` is listed under `peerDependencies`. This ensures all plugins share a single core instance at runtime while allowing independent plugin updates.

**Runtime dependencies:** Declare only required runtime libraries in each plugin's [`package.json`](https://github.com/cordiverse/cordis/blob/main/package.json). Avoid bundling `@cordisjs/core` to prevent version mismatches and memory overhead from multiple core instances.

## Summary

- **Package isolation:** Place each plugin in `packages/<name>/` with its own [`package.json`](https://github.com/cordiverse/cordis/blob/main/package.json) and use the `@cordisjs/plugin-<name>` naming convention.
- **TypeScript paths:** Configure `"@cordisjs/plugin-*": ["./packages/*/src"]` in root [`tsconfig.json`](https://github.com/cordiverse/cordis/blob/main/tsconfig.json) to enable clean imports.
- **Clean exports:** Expose functionality through [`src/index.ts`](https://github.com/cordiverse/cordis/blob/main/src/index.ts) without side effects, returning functions or classes rather than auto-registering.
- **Dynamic composition:** Use the `Loader` plugin for runtime resolution and `group` for logical bundling of plugins.
- **Peer dependencies:** List `@cordisjs/core` as a `peerDependency` to ensure singleton core instances across all plugins.
- **Colocated tests:** Store tests in `packages/<name>/tests/` and import via package names to simulate real-world usage.

## Frequently Asked Questions

### How do I scaffold a new plugin in a Cordis monorepo?

Create a directory under `packages/<plugin-name>/` containing [`src/index.ts`](https://github.com/cordiverse/cordis/blob/main/src/index.ts) and a [`package.json`](https://github.com/cordiverse/cordis/blob/main/package.json) with `"name": "@cordisjs/plugin-<plugin-name>"`. Add `@cordisjs/core` to `peerDependencies`, then update the root [`tsconfig.json`](https://github.com/cordiverse/cordis/blob/main/tsconfig.json) paths if your naming follows the standard convention. The Yarn workspace configuration will automatically recognize the new package.

### What is the difference between the Loader and Group plugins?

The `Loader` plugin dynamically resolves and imports plugins by string name at runtime, enabling configuration-driven architectures. The `Group` plugin, defined in [`packages/group/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/group/src/index.ts), statically combines multiple plugin references into a single callable unit for organizational purposes. Use `Loader` for modularity and `Group` for composition.

### Should Cordis plugins use dependencies or peerDependencies for the core framework?

Always declare `@cordisjs/core` as a `peerDependency` in your plugin's [`package.json`](https://github.com/cordiverse/cordis/blob/main/package.json). This prevents npm from installing separate copies of the core framework for each plugin, ensuring all plugins interact with the same `Context` instance and avoiding runtime conflicts.

### How does TypeScript resolve @cordisjs/plugin-* imports during development?

The root [`tsconfig.json`](https://github.com/cordiverse/cordis/blob/main/tsconfig.json) maps the wildcard pattern `@cordisjs/plugin-*` to `./packages/*/src`, allowing TypeScript to resolve imports locally without publishing or relative path tricks. This mapping enables IDE features like "Go to Definition" and auto-import to work across package boundaries as if the modules were already published to npm.